mirror of
https://github.com/nlohmann/json.git
synced 2026-10-15 18:57:13 +00:00
* Speed up the lexer: own float parser, string scan, and \u table Give the library its own correctly rounded float converter for binary32 and binary64 (IEEE 754), and speed up the lexer's string and escape scanning. The converter splits a number token into sign, significand, and decimal exponent, then tries Clinger's fast path, then a templated Eisel-Lemire step, and falls back to an exact big-integer digit comparison for tokens with more than 19 significant digits whose two candidate values round differently. This replaces std::from_chars and strtod/strtof for both formats, so parsed values no longer depend on the C/C++ library or the current locale. The strtold fallback kept for other long double formats (x87, binary128) now also copies a multi-byte decimal point correctly, fixing #5660. eisel_lemire() and decimal_to_float() are always inlined so callers keep the whole conversion in their hot loop. The string-scanning kernels in string_scan.hpp find a stop byte with the trailing-zero count of the SWAR mask instead of a byte loop, and scalar_string_bulk_run() validates a run of multi-byte UTF-8 sequences one after another instead of re-searching after each one. get_codepoint() decodes a contiguous \uXXXX escape with one table lookup per byte instead of four range-checked get() calls; the streaming path and all error positions are unchanged. Adds 508 generated hard float-parsing cases with expected binary32 and binary64 bits, and kernel-comparison tests for the string scans and the escape table against byte-by-byte references. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix CI warnings in own float parser - pow5_table.hpp: pow5_128_largest_power was unused in this branch's own code (GCC -Werror=unused-const-variable); tie it to the table size with a static_assert instead of removing it, since a later branch in the stack (json-view/23-zmij) uses it. - number_parse.hpp: rename the local variable `copy` to `buffer` to satisfy cpplint's build/include_what_you_use check. - unit-class_lexer.cpp: extend the NOLINT list on the seeded mt19937 with bugprone-random-generator-seed, and parenthesize `8 * sizeof(Bits) - 1` for clang-tidy. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Load string scan words with memcpy and use MSVC bit-scan intrinsics Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Reword float conversion notes for the string type and number_float_t docs Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy 22 findings in unit-class_lexer.cpp Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Mark the unsigned long of the MSVC bit scans for cpplint Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate the amalgamated headers Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Drop the position updates in the bulk \u path, which #5809 made unused Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address review: std::any_of in has_nonzero_digit, comment punctuation Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use data() instead of c_str() in the multi-byte decimal point path Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
498 lines
16 KiB
C++
498 lines
16 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"
|
|
|
|
#define JSON_TESTS_PRIVATE
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <array>
|
|
#include <clocale>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <ostream>
|
|
#include <streambuf>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
struct ParserImpl final: public nlohmann::json_sax<json>
|
|
{
|
|
bool null() override
|
|
{
|
|
return true;
|
|
}
|
|
bool boolean(bool /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t /*val*/, const json::string_t& s) override
|
|
{
|
|
float_string_copy = s;
|
|
return true;
|
|
}
|
|
bool string(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool key(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_object() override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_array() override
|
|
{
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*val*/, const std::string& /*val*/, const nlohmann::detail::exception& /*val*/) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
~ParserImpl() override;
|
|
|
|
ParserImpl()
|
|
: float_string_copy("not set")
|
|
{}
|
|
|
|
ParserImpl(const ParserImpl& other)
|
|
: float_string_copy(other.float_string_copy)
|
|
{}
|
|
|
|
ParserImpl(ParserImpl&& other) noexcept
|
|
: float_string_copy(std::move(other.float_string_copy))
|
|
{}
|
|
|
|
ParserImpl& operator=(const ParserImpl& other)
|
|
{
|
|
if (this != &other)
|
|
{
|
|
float_string_copy = other.float_string_copy;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
ParserImpl& operator=(ParserImpl&& other) noexcept
|
|
{
|
|
if (this != &other)
|
|
{
|
|
float_string_copy = std::move(other.float_string_copy);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
json::string_t float_string_copy;
|
|
};
|
|
|
|
ParserImpl::~ParserImpl() = default;
|
|
|
|
TEST_CASE("locale-dependent test (LC_NUMERIC=C)")
|
|
{
|
|
WARN_MESSAGE(std::setlocale(LC_NUMERIC, "C") != nullptr, "could not set locale");
|
|
|
|
SECTION("check if locale is properly set")
|
|
{
|
|
std::array<char, 6> buffer = {};
|
|
CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
CHECK(std::string(buffer.data()) == "12.34");
|
|
}
|
|
|
|
SECTION("parsing")
|
|
{
|
|
CHECK(json::parse("12.34").dump() == "12.34");
|
|
}
|
|
|
|
SECTION("SAX parsing")
|
|
{
|
|
ParserImpl sax {};
|
|
json::sax_parse( "12.34", &sax );
|
|
CHECK(sax.float_string_copy == "12.34");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("locale-dependent test (LC_NUMERIC=de_DE)")
|
|
{
|
|
if (std::setlocale(LC_NUMERIC, "de_DE") != nullptr)
|
|
{
|
|
SECTION("check if locale is properly set")
|
|
{
|
|
std::array<char, 6> buffer = {};
|
|
CHECK(std::snprintf(buffer.data(), buffer.size(), "%.2f", 12.34) == 5); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
|
|
const auto snprintf_result = std::string(buffer.data());
|
|
if (snprintf_result != "12,34")
|
|
{
|
|
CAPTURE(snprintf_result)
|
|
MESSAGE("To test if number parsing is locale-independent, we set the locale to de_DE. However, on this system, the decimal separator doesn't change to `,` potentially due to a known musl issue (https://github.com/nlohmann/json/issues/4767).");
|
|
}
|
|
}
|
|
|
|
SECTION("parsing")
|
|
{
|
|
CHECK(json::parse("12.34").dump() == "12.34");
|
|
}
|
|
|
|
SECTION("SAX parsing")
|
|
{
|
|
ParserImpl sax{};
|
|
json::sax_parse("12.34", &sax);
|
|
CHECK(sax.float_string_copy == "12.34");
|
|
}
|
|
|
|
SECTION("serializing a long double")
|
|
{
|
|
// a floating-point type that is not a float or a double is written
|
|
// with snprintf, whose locale-specific decimal point and thousands
|
|
// separator are undone afterwards
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
CHECK(long_double_json(12345.5L).dump() == "12345.5");
|
|
CHECK(long_double_json(1.0L).dump() == "1.0");
|
|
CHECK(long_double_json(-0.25L).dump() == "-0.25");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MESSAGE("locale de_DE is not usable");
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// records the numbers of a flat array and switches LC_NUMERIC to the given
|
|
// locale once the array opens - after the lexer was constructed, but before
|
|
// any number in the array is lexed
|
|
struct LocaleSwitchingSax final: public nlohmann::json_sax<json>
|
|
{
|
|
explicit LocaleSwitchingSax(const char* switch_to)
|
|
: locale_after_open(switch_to)
|
|
{}
|
|
|
|
bool null() override
|
|
{
|
|
return true;
|
|
}
|
|
bool boolean(bool /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_integer(json::number_integer_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_unsigned(json::number_unsigned_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool number_float(json::number_float_t val, const json::string_t& s) override
|
|
{
|
|
values.push_back(val);
|
|
strings.push_back(s);
|
|
return true;
|
|
}
|
|
bool string(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool binary(json::binary_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_object(std::size_t /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool key(json::string_t& /*val*/) override
|
|
{
|
|
return true;
|
|
}
|
|
bool end_object() override
|
|
{
|
|
return true;
|
|
}
|
|
bool start_array(std::size_t /*val*/) override
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, locale_after_open.c_str()) != nullptr;
|
|
return true;
|
|
}
|
|
bool end_array() override
|
|
{
|
|
return true;
|
|
}
|
|
bool parse_error(std::size_t /*val*/, const std::string& /*val*/, const nlohmann::detail::exception& /*val*/) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::string locale_after_open;
|
|
bool switched = false;
|
|
std::vector<json::number_float_t> values {}; // NOLINT(readability-redundant-member-init)
|
|
std::vector<json::string_t> strings {}; // NOLINT(readability-redundant-member-init)
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("locale changes between lexer construction and number conversion (#5198)")
|
|
{
|
|
// float and double are converted without the locale. A long double that
|
|
// is not binary64 can take the strtold fallback, which honors the locale
|
|
// that is current at conversion time. The numbers are chosen so that it
|
|
// does: too many significant digits for Clinger's fast path, an underflow
|
|
// that std::from_chars rejects, and a plain value.
|
|
const std::vector<std::string> numbers = {"3.14159265358979323846", "1.5e-400", "12.34", "-0.000123456789012345678"};
|
|
std::string text = "[";
|
|
for (const auto& n : numbers)
|
|
{
|
|
text += (text.size() == 1 ? "" : ",") + n;
|
|
}
|
|
text += "]";
|
|
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
|
|
// reference values, parsed without a locale switch
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
const json expected = json::parse(text);
|
|
const long_double_json expected_ld = long_double_json::parse(text);
|
|
|
|
const std::array<std::pair<const char*, const char*>, 2> transitions =
|
|
{
|
|
{
|
|
{"C", "de_DE"},
|
|
{"de_DE", "C"}
|
|
}
|
|
};
|
|
|
|
for (const auto& transition : transitions)
|
|
{
|
|
CAPTURE(transition.first)
|
|
CAPTURE(transition.second)
|
|
|
|
if (std::setlocale(LC_NUMERIC, transition.first) == nullptr)
|
|
{
|
|
MESSAGE("locale is not usable");
|
|
continue;
|
|
}
|
|
|
|
// SAX parsing
|
|
{
|
|
LocaleSwitchingSax sax(transition.second);
|
|
CHECK(json::sax_parse(text, &sax));
|
|
if (sax.switched)
|
|
{
|
|
CHECK(sax.values == expected.get<std::vector<json::number_float_t>>());
|
|
CHECK(sax.strings == numbers);
|
|
}
|
|
}
|
|
|
|
// DOM parsing with a callback
|
|
{
|
|
bool switched = false;
|
|
const auto cb = [&](int /*depth*/, json::parse_event_t event, json& /*parsed*/) noexcept
|
|
{
|
|
if (event == json::parse_event_t::array_start)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, transition.second) != nullptr;
|
|
}
|
|
return true;
|
|
};
|
|
const json j = json::parse(text, cb);
|
|
if (switched)
|
|
{
|
|
CHECK(j == expected);
|
|
}
|
|
}
|
|
|
|
// a long double goes through std::strtold unless it is binary64 or
|
|
// std::from_chars supports it
|
|
{
|
|
bool switched = false;
|
|
const auto cb = [&](int /*depth*/, long_double_json::parse_event_t event, long_double_json& /*parsed*/) noexcept
|
|
{
|
|
if (event == long_double_json::parse_event_t::array_start)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, transition.second) != nullptr;
|
|
}
|
|
return true;
|
|
};
|
|
const long_double_json j = long_double_json::parse(text, cb);
|
|
if (switched)
|
|
{
|
|
CHECK(j == expected_ld);
|
|
}
|
|
}
|
|
}
|
|
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
}
|
|
|
|
TEST_CASE("locale with a multi-byte decimal point")
|
|
{
|
|
// Some locales use a decimal point that is not a single character, e.g.
|
|
// U+066B ARABIC DECIMAL SEPARATOR (two bytes in UTF-8). It cannot be
|
|
// substituted in place for '.', so the strtold fallback (only for long
|
|
// double formats other than binary64) converts a copy of the token with
|
|
// the whole decimal point instead (#5660). The values must be those of the
|
|
// "C" locale.
|
|
using long_double_json = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, long double>;
|
|
const char* const long_double_numbers = "[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]";
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
const long_double_json expected_long_double = long_double_json::parse(long_double_numbers);
|
|
|
|
const std::array<const char*, 6> names = {{"ar_EG.UTF-8", "ar_SA.UTF-8", "fa_IR.UTF-8", "ps_AF.UTF-8", "ar_EG", "fa_IR"}};
|
|
bool tested = false;
|
|
for (const char* name : names)
|
|
{
|
|
if (std::setlocale(LC_NUMERIC, name) == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const std::string decimal_point = std::localeconv()->decimal_point;
|
|
if (decimal_point.size() < 2)
|
|
{
|
|
continue;
|
|
}
|
|
CAPTURE(name)
|
|
tested = true;
|
|
|
|
// too many significant digits for Clinger's fast path, and an underflow
|
|
// that std::from_chars rejects: double does not depend on the locale
|
|
json j;
|
|
CHECK_NOTHROW(j = json::parse("[3.14159265358979323846, 1.5e-400, -0.000123456789012345678]"));
|
|
CHECK(j.is_array());
|
|
CHECK(j[0] == 3.14159265358979323846);
|
|
CHECK(j[1] == 0.0);
|
|
CHECK(j[2] == -0.000123456789012345678);
|
|
CHECK(json::accept("3.14159265358979323846"));
|
|
|
|
// a long double that reaches the strtold fallback is not truncated
|
|
long_double_json ld;
|
|
CHECK_NOTHROW(ld = long_double_json::parse(long_double_numbers));
|
|
CHECK(ld == expected_long_double);
|
|
|
|
// a value the locale-independent paths convert is not affected
|
|
CHECK(json::parse("12.5") == 12.5);
|
|
}
|
|
if (!tested)
|
|
{
|
|
MESSAGE("no locale with a multi-byte decimal point is usable");
|
|
}
|
|
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// a streambuf that switches LC_NUMERIC the first time anything is written to
|
|
// it, so a dump() in progress can be made to change locale mid-flight: after
|
|
// the serializer was constructed (and, before #5709 item 3, after it had
|
|
// cached std::localeconv() for the whole call) but before a later float is
|
|
// converted
|
|
struct LocaleSwitchingStreambuf final : std::streambuf
|
|
{
|
|
explicit LocaleSwitchingStreambuf(const char* switch_to)
|
|
: locale_after_first_write(switch_to)
|
|
{}
|
|
|
|
std::string data {}; // NOLINT(readability-redundant-member-init)
|
|
std::string locale_after_first_write;
|
|
bool switched = false;
|
|
|
|
protected:
|
|
std::streamsize xsputn(const char* s, std::streamsize n) override
|
|
{
|
|
if (!switched)
|
|
{
|
|
switched = std::setlocale(LC_NUMERIC, locale_after_first_write.c_str()) != nullptr;
|
|
}
|
|
data.append(s, static_cast<std::size_t>(n));
|
|
return n;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("locale changes during a single dump() (#5709 item 3)")
|
|
{
|
|
// dump_float() only reads the locale on the snprintf path, taken for a
|
|
// number_float_t that is not an IEEE-754 single or double, i.e. not
|
|
// (is_iec559 && digits == 24 && max_exponent == 128) and not (is_iec559
|
|
// && digits == 53 && max_exponent == 1024) - see dump_float(). Checking
|
|
// is_iec559 alone is not enough: on x86_64, long double is a 64-bit
|
|
// (80-bit extended) format for which is_iec559 is also true, so it still
|
|
// takes the snprintf path this test means to exercise. Only a
|
|
// number_float_t whose digits/max_exponent match float or double (e.g.
|
|
// long double on 64-bit Arm, where it is IEEE-754 double) takes the
|
|
// locale-independent to_chars() path instead, and this test is a no-op
|
|
// there.
|
|
using long_double_json = nlohmann::json::with_float_t<long double>;
|
|
using ld_limits = std::numeric_limits<long_double_json::number_float_t>;
|
|
const bool is_ieee_single_or_double =
|
|
(ld_limits::is_iec559 && ld_limits::digits == 24 && ld_limits::max_exponent == 128) ||
|
|
(ld_limits::is_iec559 && ld_limits::digits == 53 && ld_limits::max_exponent == 1024);
|
|
if (is_ieee_single_or_double)
|
|
{
|
|
MESSAGE("long double is IEEE-754 single or double on this platform; dump_float()'s snprintf/locale path is not exercised here");
|
|
}
|
|
|
|
const char* de_DE_name = "de_DE.UTF-8";
|
|
if (std::setlocale(LC_NUMERIC, de_DE_name) == nullptr)
|
|
{
|
|
de_DE_name = "de_DE";
|
|
if (std::setlocale(LC_NUMERIC, de_DE_name) == nullptr)
|
|
{
|
|
MESSAGE("locale de_DE is not usable");
|
|
return;
|
|
}
|
|
}
|
|
const std::string decimal_point = std::localeconv()->decimal_point;
|
|
REQUIRE(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
if (decimal_point != ",")
|
|
{
|
|
MESSAGE("de_DE's decimal point is not ',' on this platform, skipping");
|
|
return;
|
|
}
|
|
|
|
// a string long enough to overflow the serializer's internal write
|
|
// buffer, so that it is flushed to the output adapter - and the locale
|
|
// switched - before the number after it is converted
|
|
const std::string padding(5000, 'a');
|
|
const long_double_json j = { padding, 1234.5L };
|
|
|
|
LocaleSwitchingStreambuf buf(de_DE_name);
|
|
std::ostream os(&buf);
|
|
os << j;
|
|
CHECK(std::setlocale(LC_NUMERIC, "C") != nullptr);
|
|
|
|
REQUIRE(buf.switched);
|
|
// whatever locale was in effect when the float was actually converted,
|
|
// the output is normalized to use '.' as the decimal point: it must be
|
|
// looked up at conversion time, not once for the whole dump() - the same
|
|
// fix #5597 made on the parser side
|
|
CHECK(buf.data == "[\"" + padding + "\",1234.5]");
|
|
}
|