mirror of
https://github.com/nlohmann/json.git
synced 2026-09-30 14:05:18 +00:00
Add the one-pass parser of json_view (internal)
The builder parses JSON text in one pass into the node index: strings and numbers stay in the source (escaped strings are decoded into an arena), integers are converted while their digits are in cache, and floats keep their digit layout for a later conversion. It accepts exactly what json::parse accepts, for every combination of comments and trailing commas, with and without a terminating NUL, and with JSON_STRICT_NUL_HANDLING. Parse state lives in a local cursor whose address never escapes, so that it stays in registers; out-of-line helpers (errors, regrowth, escapes, comments) are members of the builder and get the positions they need. The value dispatch is expanded once for array elements and once for member values. Literals are compared with memcmp and words read in a fixed byte order, so nothing depends on the platform's byte order. Error messages come with the public classes. Tests (unit-json_view_builder.cpp): accept/reject and values against json::parse for handwritten, generated, and damaged documents under all option combinations, from std::string and from exact-size buffers (no read past the input under AddressSanitizer), deep nesting up to 100,000 levels, NUL/BOM/whitespace cases, and the test-suite files. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -66,6 +66,7 @@ cc_library(
|
||||
"include/nlohmann/detail/string_escape.hpp",
|
||||
"include/nlohmann/detail/string_utils.hpp",
|
||||
"include/nlohmann/detail/value_t.hpp",
|
||||
"include/nlohmann/detail/view/builder.hpp",
|
||||
"include/nlohmann/detail/view/document_data.hpp",
|
||||
"include/nlohmann/detail/view/macro_scope.hpp",
|
||||
"include/nlohmann/detail/view/macro_unscope.hpp",
|
||||
|
||||
1026
include/nlohmann/detail/view/builder.hpp
Normal file
1026
include/nlohmann/detail/view/builder.hpp
Normal file
File diff suppressed because it is too large
Load Diff
327
tests/src/unit-json_view_builder.cpp
Normal file
327
tests/src/unit-json_view_builder.cpp
Normal file
@@ -0,0 +1,327 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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.hpp>
|
||||
#include <nlohmann/detail/view/builder.hpp>
|
||||
using nlohmann::json;
|
||||
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <test_data.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
using nlohmann::detail::view::document_data;
|
||||
using nlohmann::detail::view::node;
|
||||
|
||||
// the node index of a text; a vector input has no terminating NUL, so that
|
||||
// AddressSanitizer catches any read past the last byte
|
||||
struct built
|
||||
{
|
||||
std::unique_ptr<document_data, document_data::deleter> data{}; // NOLINT(readability-redundant-member-init)
|
||||
std::vector<char> copy{}; // NOLINT(readability-redundant-member-init)
|
||||
bool ok = false;
|
||||
nlohmann::detail::view::parse_failure failure{};
|
||||
};
|
||||
|
||||
template<typename FloatType = double>
|
||||
built build(const std::string& text, bool comments, bool trailing_commas, bool sentinel)
|
||||
{
|
||||
built r;
|
||||
r.data.reset(document_data::create(nlohmann::detail::view::estimate_nodes(text.data(), text.size())));
|
||||
const char* src = text.c_str();
|
||||
if (!sentinel)
|
||||
{
|
||||
r.copy.assign(text.begin(), text.end());
|
||||
src = r.copy.data();
|
||||
}
|
||||
r.ok = nlohmann::detail::view::build < FloatType, !nlohmann::detail::abi_config::strict_nul_handling > (*r.data, src, text.size(), comments, trailing_commas, sentinel, r.failure);
|
||||
r.data->src = src;
|
||||
r.data->base[0] = src;
|
||||
r.data->base[1] = r.data->arena.data();
|
||||
return r;
|
||||
}
|
||||
|
||||
// the value of a subtree, as json::parse would build it
|
||||
json value_of(const document_data& d, const node*& n)
|
||||
{
|
||||
const node& x = *n;
|
||||
++n;
|
||||
switch (static_cast<json::value_t>(x.kind))
|
||||
{
|
||||
case json::value_t::object:
|
||||
{
|
||||
json o = json::object();
|
||||
const node* const end = &x + x.next;
|
||||
while (n != end)
|
||||
{
|
||||
const std::string key(d.str(*n), n->len);
|
||||
++n;
|
||||
o[key] = value_of(d, n);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
case json::value_t::array:
|
||||
{
|
||||
json a = json::array();
|
||||
const node* const end = &x + x.next;
|
||||
while (n != end)
|
||||
{
|
||||
a.push_back(value_of(d, n));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
case json::value_t::string:
|
||||
return std::string(d.str(x), x.len);
|
||||
case json::value_t::boolean:
|
||||
return (x.flags & nlohmann::detail::view::node_flags::is_true) != 0;
|
||||
case json::value_t::number_integer:
|
||||
return static_cast<std::int64_t>(nlohmann::detail::view::integer_bits(x));
|
||||
case json::value_t::number_unsigned:
|
||||
return nlohmann::detail::view::integer_bits(x);
|
||||
case json::value_t::number_float:
|
||||
return json::parse(std::string(d.src + x.off, x.len)).get<double>();
|
||||
case json::value_t::null:
|
||||
case json::value_t::binary:
|
||||
case json::value_t::discarded:
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
json value_of(const built& b)
|
||||
{
|
||||
const node* n = b.data->tape;
|
||||
json v = value_of(*b.data, n);
|
||||
CHECK(n == b.data->tape + b.data->tape_size);
|
||||
return v;
|
||||
}
|
||||
|
||||
// accept/reject and the value must match json::parse, for all options and
|
||||
// with and without a NUL after the text
|
||||
void check_same(const std::string& text)
|
||||
{
|
||||
CAPTURE(text);
|
||||
for (int options = 0; options < 4; ++options)
|
||||
{
|
||||
const bool comments = (options & 1) != 0;
|
||||
const bool trailing_commas = (options & 2) != 0;
|
||||
const bool accepted = json::accept(text, comments, trailing_commas);
|
||||
for (const bool sentinel :
|
||||
{
|
||||
true, false
|
||||
})
|
||||
{
|
||||
const built b = build(text, comments, trailing_commas, sentinel);
|
||||
CHECK(b.ok == accepted);
|
||||
if (b.ok && accepted)
|
||||
{
|
||||
CHECK(value_of(b) == json::parse(text, nullptr, true, comments, trailing_commas));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a small deterministic generator of documents
|
||||
struct generator
|
||||
{
|
||||
std::mt19937 rng{5295};
|
||||
|
||||
int r(int n)
|
||||
{
|
||||
return static_cast<int>(rng() % static_cast<unsigned>(n));
|
||||
}
|
||||
|
||||
void ws(std::string& o)
|
||||
{
|
||||
for (int n = r(4) == 0 ? r(12) : r(2); n > 0; --n)
|
||||
{
|
||||
o += " \n\t\r "[r(6)];
|
||||
}
|
||||
}
|
||||
|
||||
void str(std::string& o)
|
||||
{
|
||||
static const char* const pieces[] = {"a", "Z", " ", "~", "\\n", "\\\"", "\\\\", "\\/", "\\u00e9", "\\ud83d\\ude00", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\x7f", "\\u001f", "long enough text to leave the first 16 bytes"};
|
||||
o += '"';
|
||||
for (int n = r(3) == 0 ? r(20) : r(6); n > 0; --n)
|
||||
{
|
||||
o += pieces[r(16)];
|
||||
}
|
||||
o += '"';
|
||||
}
|
||||
|
||||
void num(std::string& o)
|
||||
{
|
||||
static const char* const numbers[] = {"0", "-0", "1", "-1", "12", "123456789", "1234567890123456789", "9223372036854775807", "-9223372036854775808",
|
||||
"9223372036854775808", "18446744073709551615", "18446744073709551616", "-9223372036854775809",
|
||||
"1.5", "-2.25e-3", "1e10", "1E+2", "0.000001", "3.141592653589793238462643", "1e308", "-1e-400", "123.456e7"
|
||||
};
|
||||
o += numbers[r(22)];
|
||||
}
|
||||
|
||||
void value(std::string& o, int depth)
|
||||
{
|
||||
ws(o);
|
||||
const int k = depth > 5 ? 2 + r(6) : r(8);
|
||||
if (k == 0 || k == 1)
|
||||
{
|
||||
const bool object = k == 0;
|
||||
o += object ? '{' : '[';
|
||||
for (int i = r(5); i > 0; --i)
|
||||
{
|
||||
ws(o);
|
||||
if (object)
|
||||
{
|
||||
str(o);
|
||||
ws(o);
|
||||
o += ':';
|
||||
}
|
||||
value(o, depth + 1);
|
||||
o += i > 1 ? "," : "";
|
||||
}
|
||||
ws(o);
|
||||
o += object ? '}' : ']';
|
||||
}
|
||||
else if (k < 4)
|
||||
{
|
||||
str(o);
|
||||
}
|
||||
else if (k < 6)
|
||||
{
|
||||
num(o);
|
||||
}
|
||||
else
|
||||
{
|
||||
static const char* const literals[] = {"true", "false", "null"};
|
||||
o += literals[r(3)];
|
||||
}
|
||||
ws(o);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("json_view builder")
|
||||
{
|
||||
SECTION("scalars and containers")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"null", "true", "false", "0", "-0", "42", "-42", "1.5", "\"\"", "\"abc\"", "[]", "{}", "[1,2,3]", "{\"a\":1,\"b\":[true,null]}",
|
||||
" [ 1 , 2 ] ", "{\"a\" : {\"b\" : {}}}", "[[[]]]", "\"\\u00e4\\n\\ud83d\\ude00\"", "{\"a\":1,\"a\":2}", "18446744073709551616",
|
||||
"-9223372036854775809", "123456789012345678901234567890", "1e400", "-1e400", "1.7976931348623157e308"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("malformed input")
|
||||
{
|
||||
for (const char* text :
|
||||
{
|
||||
"", " ", "[", "]", "{", "}", "[1,]", "{\"a\":1,}", "[1 2]", "{\"a\" 1}", "{1:2}", "tru", "nul", "fals", "truex", "-", "01", "1.", ".5", "1e", "1e+",
|
||||
"\"", "\"abc", "\"\\x\"", "\"\\u12\"", "\"\\u12G4\"", "\"\\ud800\"", "\"\\udc00\"", "\"\\ud800\\u0041\"", "\"\x01\"", "\"\xff\"", "\"\xc3\"",
|
||||
"\"\xe0\x80\x80\"", "\"\xed\xa0\x80\"", "[1]x", "[1] [2]", "/", "/*", "/* */ 1", "// c\n1", "1 // c", "[1,/*c*/2]", "[1,2,]"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("NUL, BOM, and whitespace")
|
||||
{
|
||||
check_same(std::string("[1]\0garbage", 11));
|
||||
check_same(std::string("[1\0]", 4));
|
||||
check_same(std::string("[1, // c\0\n2]", 12));
|
||||
check_same(std::string("[1, /* c\0 */ 2]", 15));
|
||||
check_same("\xEF\xBB\xBF[1]");
|
||||
check_same("\xEF\xBB[1]");
|
||||
check_same(" \t\r\n 7 \n");
|
||||
for (const char* text :
|
||||
{"[1]\r", "[1]\n", "[1]\r\n", "[1,\r2]", "[1,\r\n2]", "7\r", "\"x\"\r", "{\"a\":\r\n1}\r", "[\n 1,\n 2\n]", "{\n \"a\": [\n 1\n ]\n}"
|
||||
})
|
||||
{
|
||||
check_same(text);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
// the open containers beyond 64 levels live on the heap
|
||||
for (const std::size_t depth :
|
||||
{
|
||||
63u, 64u, 65u, 1000u, 100000u
|
||||
})
|
||||
{
|
||||
const std::string arrays = std::string(depth, '[') + std::string(depth, ']');
|
||||
const built b = build(arrays, false, false, false);
|
||||
REQUIRE(b.ok);
|
||||
CHECK(b.data->tape_size == depth);
|
||||
CHECK(b.data->tape[0].next == depth);
|
||||
std::string objects;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
objects += "{\"a\":";
|
||||
}
|
||||
objects += '1' + std::string(depth, '}');
|
||||
const built o = build(objects, false, false, false);
|
||||
REQUIRE(o.ok);
|
||||
CHECK(o.data->tape_size == (2 * depth) + 1);
|
||||
CHECK(!build(std::string(depth, '[') + std::string(depth - 1, ']'), false, false, false).ok);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("generated documents and damaged copies")
|
||||
{
|
||||
generator g;
|
||||
for (int i = 0; i < 3000; ++i)
|
||||
{
|
||||
std::string text;
|
||||
g.value(text, 0);
|
||||
check_same(text);
|
||||
// damage: flip one byte, or cut the text
|
||||
std::string damaged = text;
|
||||
const auto at = static_cast<std::size_t>(g.r(static_cast<int>(damaged.size())));
|
||||
static const char replacements[] = {'x', '"', '\\', ',', ':', ']', '}', '[', '{', '1', '-', '.', 'e', '\0', '\n', '/'};
|
||||
damaged[at] = replacements[g.r(16)];
|
||||
check_same(damaged);
|
||||
check_same(text.substr(0, at));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("test files")
|
||||
{
|
||||
for (const char* name :
|
||||
{
|
||||
"/json.org/1.json", "/json.org/2.json", "/json.org/3.json", "/json.org/4.json", "/json.org/5.json",
|
||||
"/json_testsuite/sample.json", "/nativejson-benchmark/canada.json", "/nativejson-benchmark/citm_catalog.json",
|
||||
"/nativejson-benchmark/twitter.json", "/json_tests/pass1.json", "/json_tests/pass2.json", "/json_tests/pass3.json"
|
||||
})
|
||||
{
|
||||
CAPTURE(name);
|
||||
std::ifstream f(std::string(TEST_DATA_DIRECTORY) + name, std::ios::binary);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
const std::string text = ss.str();
|
||||
REQUIRE(!text.empty());
|
||||
const built b = build(text, false, false, true);
|
||||
REQUIRE(b.ok);
|
||||
CHECK(value_of(b) == json::parse(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user