mirror of
https://github.com/nlohmann/json.git
synced 2026-10-15 18:57:13 +00:00
Parsing from a std::istream read one character at a time through the streambuf, so none of the parser's fast paths applied: it was 1.8 to 2.5 times slower than reading the file into a string and parsing that. input_stream_adapter now exposes the streambuf's get area (gptr() to egptr()) to the lexer as a window. The protected get-area members are reached through member pointers formed in a class derived from std::streambuf, which is ordinary, standard-conforming member access. The lexer scans the window with the same bulk paths as a string, and asks the streambuf for the next character as before when the window is used up. Nothing is read ahead, copied, or put back, so the stream is left exactly where it was left before, with and without JSON_PRECISE_STREAM_POSITION, and a streambuf without a get area is still read one character at a time. The adapter also keeps the current token for error messages as a pointer into the get area, and copies it out only before the get area is refilled, instead of the lexer copying every character. On an Apple M1 Max (Apple Clang, -O3), parsing from an std::ifstream is 1.6 to 2.1 times as fast (twitter.json 4.08 -> 2.30 ms, citm_catalog.json 8.75 -> 4.08 ms), and reading 100,000 small values with operator>> 1.4 (objects) to 4.0 (integers) times as fast. With GCC 13 and libstdc++ on x86-64 Linux, std::ifstream is 1.5 to 2.2 times as fast. Parsing from a stream is now within 11 to 18 % of reading the file into a string first. JSON_DISABLE_STREAM_BULK_SCAN restores reading one character at a time, for streambufs that do not follow the usual get-area contract. The new tests compare every observable result (values, exception messages, stream state, remaining characters) for get areas of 1 to 4096 bytes against a streambuf without a get area, and run again with the macro and with JSON_PRECISE_STREAM_POSITION. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
324 lines
10 KiB
C++
324 lines
10 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | 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"
|
|
|
|
// This file tests the opt-in JSON_PRECISE_STREAM_POSITION, so it defines the
|
|
// macro itself rather than relying on a -D flag, and runs in every build. The
|
|
// default behavior is pinned in unit-deserialization.cpp.
|
|
#ifdef JSON_PRECISE_STREAM_POSITION
|
|
#undef JSON_PRECISE_STREAM_POSITION
|
|
#endif
|
|
|
|
#define JSON_PRECISE_STREAM_POSITION 1
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <sstream>
|
|
#include <streambuf>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#define STRINGIZE_EX(x) #x
|
|
#define STRINGIZE(x) STRINGIZE_EX(x)
|
|
|
|
namespace
|
|
{
|
|
// A streambuf that keeps no get area at all and refuses every putback: with an
|
|
// empty get area, sungetc() always ends up in pbackfail(). Used to check that
|
|
// the character terminating a number is left in the input without relying on
|
|
// the streambuf being able to put a consumed character back.
|
|
class no_putback_streambuf : public std::streambuf
|
|
{
|
|
public:
|
|
explicit no_putback_streambuf(std::string s) : m_data(std::move(s)) {}
|
|
|
|
protected:
|
|
// peek at the next character without consuming it
|
|
int_type underflow() override
|
|
{
|
|
if (m_pos >= m_data.size())
|
|
{
|
|
return traits_type::eof();
|
|
}
|
|
return traits_type::to_int_type(m_data[m_pos]);
|
|
}
|
|
|
|
// consume the next character
|
|
int_type uflow() override
|
|
{
|
|
if (m_pos >= m_data.size())
|
|
{
|
|
return traits_type::eof();
|
|
}
|
|
return traits_type::to_int_type(m_data[m_pos++]);
|
|
}
|
|
|
|
int_type pbackfail(int_type /*c*/) override
|
|
{
|
|
return traits_type::eof();
|
|
}
|
|
|
|
private:
|
|
std::string m_data;
|
|
std::size_t m_pos = 0;
|
|
};
|
|
|
|
// A streambuf that serves its input in get areas of a fixed size, so that the
|
|
// end of a number falls on every possible offset of a get area. The lexer scans
|
|
// the get area directly; the stream position must not depend on its size.
|
|
class chunked_streambuf : public std::streambuf
|
|
{
|
|
public:
|
|
chunked_streambuf(std::string s, std::size_t chunk_size)
|
|
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
|
{}
|
|
|
|
protected:
|
|
int_type underflow() override
|
|
{
|
|
if (gptr() < egptr())
|
|
{
|
|
return traits_type::to_int_type(*gptr());
|
|
}
|
|
m_pos += static_cast<std::size_t>(egptr() - eback());
|
|
if (m_pos >= m_data.size())
|
|
{
|
|
setg(nullptr, nullptr, nullptr);
|
|
return traits_type::eof();
|
|
}
|
|
char* const begin = &m_data[m_pos];
|
|
setg(begin, begin, begin + (std::min)(m_chunk_size, m_data.size() - m_pos));
|
|
return traits_type::to_int_type(*gptr());
|
|
}
|
|
|
|
private:
|
|
std::string m_data;
|
|
std::size_t m_pos = 0;
|
|
std::size_t m_chunk_size;
|
|
};
|
|
|
|
// read the characters that are left in a stream
|
|
std::string remaining(std::istream& is)
|
|
{
|
|
std::string result;
|
|
char c = 0;
|
|
while (is.get(c))
|
|
{
|
|
result += c;
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("JSON_PRECISE_STREAM_POSITION")
|
|
{
|
|
SECTION("the macro is part of the ABI tag")
|
|
{
|
|
const std::string ns = STRINGIZE(NLOHMANN_JSON_NAMESPACE);
|
|
// other tags may come before it, e.g. json_abi_diag_psp
|
|
CHECK(ns.find("_psp") != std::string::npos);
|
|
}
|
|
|
|
SECTION("a number does not consume the character that terminates it")
|
|
{
|
|
// a number is only terminated by the character following it; that
|
|
// character must be given back so the stream is positioned right
|
|
// after the value
|
|
const std::vector<std::pair<std::string, std::string>> tests =
|
|
{
|
|
{"1true", "true"},
|
|
{"1[2]", "[2]"},
|
|
{"1{}", "{}"},
|
|
{R"(1"a")", R"("a")"},
|
|
{"1 true", " true"},
|
|
{"12,", ","},
|
|
{"-0.5e3x", "x"},
|
|
{"1null", "null"}
|
|
};
|
|
|
|
for (const auto& test : tests)
|
|
{
|
|
CAPTURE(test.first)
|
|
std::istringstream ss(test.first);
|
|
json j;
|
|
ss >> j;
|
|
CHECK(j == json::parse(test.first.substr(0, test.first.size() - test.second.size())));
|
|
CHECK(remaining(ss) == test.second);
|
|
}
|
|
}
|
|
|
|
SECTION("values that are self-delimiting are unaffected")
|
|
{
|
|
const std::vector<std::pair<std::string, std::string>> tests =
|
|
{
|
|
{"truefalse", "false"},
|
|
{"[1][2]", "[2]"},
|
|
{R"({"a":1}{"b":2})", R"({"b":2})"},
|
|
{R"("a""b")", R"("b")"},
|
|
{"null null", " null"}
|
|
};
|
|
|
|
for (const auto& test : tests)
|
|
{
|
|
CAPTURE(test.first)
|
|
std::istringstream ss(test.first);
|
|
json j;
|
|
ss >> j;
|
|
CHECK(remaining(ss) == test.second);
|
|
}
|
|
}
|
|
|
|
SECTION("a number at the end of the input leaves nothing behind")
|
|
{
|
|
for (const std::string s :
|
|
{"1", "12", "-3.5e2", " 7 "
|
|
})
|
|
{
|
|
CAPTURE(s)
|
|
std::istringstream ss(s);
|
|
json j;
|
|
ss >> j;
|
|
CHECK(remaining(ss).find_first_not_of(" \t\n\r") == std::string::npos);
|
|
}
|
|
}
|
|
|
|
SECTION("repeated extraction of concatenated values")
|
|
{
|
|
std::istringstream ss(R"(1true[2]3"x"{"a":4}5)");
|
|
const std::vector<json> expected =
|
|
{
|
|
json(1), json(true), json::parse("[2]"), json(3),
|
|
json("x"), json::parse(R"({"a":4})"), json(5)
|
|
};
|
|
|
|
for (const auto& e : expected)
|
|
{
|
|
json j;
|
|
ss >> j;
|
|
CHECK(j == e);
|
|
}
|
|
}
|
|
|
|
SECTION("differences to the default behavior")
|
|
{
|
|
// both of these work by accident without the macro, because the
|
|
// character after a number is swallowed; see unit-deserialization.cpp
|
|
|
|
SECTION("a separator after a number is not skipped")
|
|
{
|
|
std::istringstream ss("1,2");
|
|
json j;
|
|
ss >> j;
|
|
CHECK(j == 1);
|
|
CHECK_THROWS_AS(ss >> j, json::parse_error&);
|
|
}
|
|
|
|
SECTION("std::getline after a number sees the line break")
|
|
{
|
|
std::istringstream ss("42\nfoo");
|
|
json j;
|
|
std::string line;
|
|
ss >> j;
|
|
std::getline(ss, line);
|
|
CHECK(j == 42);
|
|
CHECK(line.empty());
|
|
std::getline(ss, line);
|
|
CHECK(line == "foo");
|
|
}
|
|
}
|
|
|
|
SECTION("sax_parse with strict == false")
|
|
{
|
|
std::istringstream ss("1true");
|
|
json j;
|
|
nlohmann::detail::json_sax_dom_parser<json, nlohmann::detail::input_stream_adapter> sdp(j, true);
|
|
CHECK(json::sax_parse(ss, &sdp, nlohmann::detail::input_format_t::json, false));
|
|
CHECK(j == 1);
|
|
CHECK(remaining(ss) == "true");
|
|
}
|
|
|
|
SECTION("strict parsing still rejects trailing data")
|
|
{
|
|
std::istringstream ss("1true");
|
|
json _;
|
|
CHECK_THROWS_WITH_AS(_ = json::parse(ss),
|
|
"[json.exception.parse_error.101] parse error at line 1, column 5: syntax error while parsing value - unexpected true literal; expected end of input", json::parse_error&);
|
|
|
|
std::istringstream ss2("1true");
|
|
CHECK_FALSE(json::accept(ss2));
|
|
}
|
|
|
|
SECTION("a streambuf that cannot put back is not needed")
|
|
{
|
|
// the terminating character is never consumed, so no putback
|
|
// position is required
|
|
no_putback_streambuf buf("1true");
|
|
std::istream is(&buf);
|
|
json j;
|
|
is >> j;
|
|
CHECK(j == json(1));
|
|
CHECK(remaining(is) == "true");
|
|
}
|
|
|
|
SECTION("stream with eofbit in its exception mask (issue #5646)")
|
|
{
|
|
// with JSON_PRECISE_STREAM_POSITION, get_character() peeks via
|
|
// sb->sgetc() rather than consuming via sb->sbumpc(), but it still
|
|
// calls std::istream::clear() to record eofbit once the streambuf is
|
|
// exhausted; with eofbit in the exception mask, that clear() itself
|
|
// throws std::ios_base::failure, which must propagate to the caller
|
|
// instead of ~input_stream_adapter() throwing a second exception
|
|
// while the first is still unwinding (which would call std::terminate)
|
|
json _;
|
|
|
|
std::istringstream is1("1");
|
|
is1.exceptions(std::ios::eofbit);
|
|
CHECK_THROWS_AS(_ = json::parse(is1), std::ios_base::failure&);
|
|
|
|
std::istringstream is2("1");
|
|
is2.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit);
|
|
CHECK_THROWS_AS(_ = json::parse(is2), std::ios_base::failure&);
|
|
}
|
|
|
|
SECTION("stream without a streambuf (issue #5646)")
|
|
{
|
|
// std::istream(nullptr) has badbit set and rdbuf() == nullptr;
|
|
// get_character() must not dereference that null streambuf
|
|
std::istream is(nullptr);
|
|
json _;
|
|
CHECK_THROWS_WITH_AS(_ = json::parse(is), "[json.exception.parse_error.101] parse error: attempting to parse an empty input; check that your input string or stream contains the expected JSON", json::parse_error&);
|
|
}
|
|
|
|
SECTION("the position does not depend on the get area of the streambuf")
|
|
{
|
|
const std::vector<std::size_t> chunk_sizes = {1, 2, 3, 4, 5, 7, 8, 13, 4096};
|
|
for (const std::size_t chunk_size : chunk_sizes)
|
|
{
|
|
CAPTURE(chunk_size)
|
|
chunked_streambuf buf("12345678901234567890true\"a long string value\"-1.25e+3,[1,2]42", chunk_size);
|
|
std::istream input(&buf);
|
|
json j;
|
|
|
|
input >> j;
|
|
CHECK(j == 12345678901234567890u);
|
|
input >> j;
|
|
CHECK(j == true);
|
|
input >> j;
|
|
CHECK(j == "a long string value");
|
|
input >> j;
|
|
CHECK(j == -1250.0);
|
|
CHECK(remaining(input) == ",[1,2]42");
|
|
}
|
|
}
|
|
}
|