| // __ _____ _____ _____ |
| // __| | __| | | | 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]"); |
| } |