mirror of
https://github.com/nlohmann/json.git
synced 2026-10-02 20:50:32 +00:00
Parsing from a std::istream crashed in two unusual but valid stream states, both in input_stream_adapter: - With eofbit in the stream's exceptions() mask, get_character() sets eofbit via is->clear(), which throws std::ios_base::failure. While that exception unwinds, ~input_stream_adapter() called clear() again to reset eofbit, which is still set and still in the exception mask, so it throws a second time out of the (implicitly noexcept) destructor and std::terminate() is called. The destructor now only calls clear() if a bit other than eofbit remains set, so the first exception can propagate normally. - For an std::istream without a stream buffer (rdbuf() == nullptr, e.g. std::istream(nullptr)), the constructor stored the null pointer without checking it, and get_character() dereferenced it. input_adapter(std::istream&) now throws parse_error.101 for such a stream, the same as it already does for a null FILE* or char*. Added regression tests to unit-deserialization.cpp and, for the JSON_PRECISE_STREAM_POSITION variant of get_character(), to unit-precise-stream-position.cpp; both crashed before this fix. Documented the two exceptions in parse.md and operator_gtgt.md. Fixes #5646. Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
267 lines
8.2 KiB
C++
267 lines
8.2 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 <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;
|
|
};
|
|
|
|
// 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&);
|
|
}
|
|
}
|