lexer: use std::from_chars when available, fall back to strto* otherwise

`std::from_chars` is locale-independent and thus avoids the TOCTOU
issue #5198 when it's available.

This commit introduces a `from_chars` utility in the `detail` namespace
which merely wraps `std::from_chars` if it is available and emulates its
API and behavior using the `strto*` family of functions when it is not.
As of this commit, the `strto*`-based fallback is still locale-dependent
and only matches `std::from_chars` when the "C" locale is used.

Availability of `std::from_chars` for floating point numbers is varied.
For instance, as of libc++ 22, `std::from_chars` for `long double` isn't
supported and, consequently, the feature-test macro `__cpp_lib_to_chars`
is not set, while the overloads for `float` and `double` are available.
To avoid complicating matters further, the feature-test macro is taken
as the authoritative indicator for (full) `std::from_chars` support and
the fallback is used otherwise.

If `std::from_chars` is available, our wrapper additionally tweaks its
output in the case of floating-point out-of-range results. The behavior
in this case is poorly (under-)specified and existing implementations do
not agree. The wrapper's tweaks are intended to ensure consistent
behavior across all toolchains in this edge case. In practice, this
should only take effect when parsing out-of-range floats on libstdc++.

The unit tests for the `from_chars` helper are intentionally compiled in
both C++11 and C++17 modes. This ensures that the test expectations
align with the actual behavior we're trying to model. The tests are not
aiming to cover all of floating-point parsing comprehensively, as
`std::from_chars` is specified to behave mostly the same as `strto*`
and instead focuses on the edge cases where the two differ:
- leading whitespace is not tolerated
- `+` sign is not allowed (outside of the exponent)
- out-parameter is left unchanged in case of `invalid_argument` and
  integral `result_out_of_range` errors

Signed-off-by: Jonas Greitemann <jgreitemann@gmail.com>
This commit is contained in:
Jonas Greitemann
2026-07-21 21:34:56 +02:00
parent 6fb162a394
commit 85839d7408
8 changed files with 1083 additions and 90 deletions
+2
View File
@@ -55,6 +55,8 @@ TEST_CASE("lexer class")
SECTION("numbers")
{
// Number parsing implementation uses std::from_chars where available,
// so run this test suite with JSON_HAS_CPP_17 as well.
CHECK((scan_string("0") == json::lexer::token_type::value_unsigned));
CHECK((scan_string("1") == json::lexer::token_type::value_unsigned));
CHECK((scan_string("2") == json::lexer::token_type::value_unsigned));
+278
View File
@@ -0,0 +1,278 @@
// __ _____ _____ _____
// __| | __| | | | 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 <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <system_error>
// nlohmann::details::from_chars is a wrapper around std::from_chars when it is
// available (__cpp_lib_to_chars is defined), otherwise our fallback
// implementation kicks in.
//
// Due to the incomplete implementation of this C++17 standard library feature
// (for example, lack of support for long double even in current libc++),
// JSON_HAS_CPP_17 does not guarantee that std::from_chars is available.
// However, the reverse holds: If the test is compiled in C++11 mode, the
// fallback implementation will be used for sure.
// By mentioning the JSON_HAS_CPP_17 macro in this here comment, the test will
// be compiled both using our fallback and, at least on some platforms,
// the C++17 standard library version, as a way of ensuring that the
// expectations formulated in this test align with `std::from_chars`.
namespace
{
struct init_val_t {};
constexpr init_val_t init_val{};
template <typename T>
void check_result(std::string& str, T& value, std::ptrdiff_t expected_ptr_offset, std::errc expected_ec)
{
// On platforms where `std::from_chars` is not available, our `strtof`-based implementation is
// in fact locale-dependent and might use a different decimal separator, so we need to adapt
// the test expectations accordingly.
std::replace(str.begin(), str.end(), '.', nlohmann::detail::from_chars_traits<T>::get_decimal_point());
auto res = nlohmann::detail::from_chars(str.data(), str.data() + str.size(), value);
CHECK_MESSAGE(res.ec == expected_ec, "Error code mismatch while parsing: \"", str,
"\": Actual: ", make_error_code(res.ec).message(),
"; expected: ", make_error_code(expected_ec).message());
CHECK_MESSAGE(res.ptr - str.data() == expected_ptr_offset, "Ptr offset mismatch while parsing: \"", str, "\"");
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
check(std::string str, T expected_value, std::ptrdiff_t expected_ptr_offset, std::errc expected_ec)
{
T value = 42;
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(value == expected_value, "while parsing: \"", str, "\"");
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
check(std::string str, T expected_value, std::ptrdiff_t expected_ptr_offset, std::errc expected_ec)
{
{
T value = 42;
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(value == expected_value, "while parsing: \"", str, "\"");
CHECK_MESSAGE(std::signbit(value) == std::signbit(expected_value), "while parsing: \"", str, "\"");
}
{
T value = std::numeric_limits<T>::quiet_NaN();
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(value == expected_value, "while parsing: \"", str, "\"");
}
}
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
check(std::string str, init_val_t, std::ptrdiff_t expected_ptr_offset, std::errc expected_ec)
{
T const init_value = 42;
T value = init_value;
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(value == init_value, "while parsing: \"", str, "\"");
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
check(std::string str, init_val_t, std::ptrdiff_t expected_ptr_offset, std::errc expected_ec)
{
{
T const init_value = 42;
T value = init_value;
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(value == init_value, "while parsing: \"", str, "\"");
}
{
T value = std::numeric_limits<T>::quiet_NaN();
check_result(str, value, expected_ptr_offset, expected_ec);
CHECK_MESSAGE(doctest::IsNaN<T>(value), "while parsing: \"", str, "\"");
}
}
} // namespace
TEST_CASE("integral results consistent with std::from_chars")
{
SECTION("unsigned long long")
{
check<unsigned long long>("", init_val, 0, std::errc::invalid_argument);
check<unsigned long long>("0", 0ULL, 1, std::errc{});
check<unsigned long long>(" 123", init_val, 0, std::errc::invalid_argument);
check<unsigned long long>("123 ", 123ULL, 3, std::errc{});
check<unsigned long long>("+123", init_val, 0, std::errc::invalid_argument);
check<unsigned long long>("-123", init_val, 0, std::errc::invalid_argument);
check<unsigned long long>("123", 123ULL, 3, std::errc{});
check<unsigned long long>("123e10", 123ULL, 3, std::errc{});
check<unsigned long long>("18446744073709551615", 18446744073709551615ULL, 20, std::errc{});
check<unsigned long long>("18446744073709551616", init_val, 20, std::errc::result_out_of_range);
}
SECTION("long long")
{
check<long long>("", init_val, 0, std::errc::invalid_argument);
check<long long>("0", 0LL, 1, std::errc{});
check<long long>(" 123", init_val, 0, std::errc::invalid_argument);
check<long long>("123 ", 123LL, 3, std::errc{});
check<long long>("+123", init_val, 0, std::errc::invalid_argument);
check<long long>("-123", -123LL, 4, std::errc{});
check<long long>("123", 123LL, 3, std::errc{});
check<long long>("123e10", 123LL, 3, std::errc{});
check<long long>("9223372036854775807", 9223372036854775807LL, 19, std::errc{});
check<long long>("9223372036854775808", init_val, 19, std::errc::result_out_of_range);
check<long long>("-9223372036854775808", -9223372036854775807LL - 1, 20, std::errc{});
check<long long>("-9223372036854775809", init_val, 20, std::errc::result_out_of_range);
}
}
TEST_CASE("floating point results consistent with std::from_chars")
{
INFO("Using decimal point '",
std::string{1, nlohmann::detail::from_chars_traits<float>::get_decimal_point()},
"' to match our possibly-locale-dependent from_chars fallback implementation");
SECTION("single precision")
{
check<float>("", init_val, 0, std::errc::invalid_argument);
check<float>(" 123", init_val, 0, std::errc::invalid_argument);
check<float>("123 ", 123.0f, 3, std::errc{});
check<float>("+123", init_val, 0, std::errc::invalid_argument);
check<float>("-123", -123.0f, 4, std::errc{});
check<float>("123", 123.0f, 3, std::errc{});
check<float>("123e10", 123e10f, 6, std::errc{});
check<float>("123e+10", 123e10f, 7, std::errc{});
check<float>("123e-10", 123e-10f, 7, std::errc{});
check<float>("123.456", 123.456f, 7, std::errc{});
check<float>("123;456", 123.0f, 3, std::errc{});
check<float>("123.456 ", 123.456f, 7, std::errc{});
check<float>("123;456 ", 123.0f, 3, std::errc{});
check<float>("123456789.123456789", 123456789.123456789f, 19, std::errc{});
check<float>("1e40", std::numeric_limits<float>::infinity(), 4, std::errc::result_out_of_range);
check<float>("-1e40", -std::numeric_limits<float>::infinity(), 5, std::errc::result_out_of_range);
check<float>("2e308", std::numeric_limits<float>::infinity(), 5, std::errc::result_out_of_range);
check<float>("-2e308", -std::numeric_limits<float>::infinity(), 6, std::errc::result_out_of_range);
check<float>("123.456e-789", 0.0f, 12, std::errc::result_out_of_range);
check<float>("-123.456e-789", -0.0f, 13, std::errc::result_out_of_range);
check<float>("1e-45", 1e-45f, 5, std::errc{});
check<float>("1e-46", 0.0f, 5, std::errc::result_out_of_range);
check<float>("1E-45", 1e-45f, 5, std::errc{});
check<float>("1E-46", 0.0f, 5, std::errc::result_out_of_range);
check<float>("10e-46", 1e-45f, 6, std::errc{});
check<float>("0.1e-45", 0.0f, 7, std::errc::result_out_of_range);
check<float>("100000000000000000000000000000000000000", 1e38f, 39, std::errc{});
check<float>("100000000000000000000000000000000000000.0", 1e38f, 41, std::errc{});
check<float>("1000000000000000000000000000000000000000e-1", 1e38f, 43, std::errc{});
check<float>("0.0000000000000000000000000000000000000000000001e84", 1e38f, 51, std::errc{});
check<float>("0.000000000000000000000000000000000000000000001", 1e-45f, 47, std::errc{});
check<float>("00.000000000000000000000000000000000000000000001", 1e-45f, 48, std::errc{});
check<float>("0.0000000000000000000000000000000000000000000001e1", 1e-45f, 50, std::errc{});
check<float>("1000000000000000000000000000000000000000e-84", 1e-45f, 44, std::errc{});
check<float>("1000000000000000000000000000000000000000", std::numeric_limits<float>::infinity(), 40, std::errc::result_out_of_range);
check<float>("1000000000000000000000000000000000000000.0", std::numeric_limits<float>::infinity(), 42, std::errc::result_out_of_range);
check<float>("100000000000000000000000000000000000000e1", std::numeric_limits<float>::infinity(), 41, std::errc::result_out_of_range);
check<float>("0.0000000000000000000000000000000000000000000001e85", std::numeric_limits<float>::infinity(), 51, std::errc::result_out_of_range);
check<float>("1000000000000000000000000000000000000000e-85", 0.0f, 44, std::errc::result_out_of_range);
check<float>("0.0000000000000000000000000000000000000000000001", 0.0f, 48, std::errc::result_out_of_range);
check<float>("0.000000000000000000000000000000000000000000001e-1", 0.0f, 50, std::errc::result_out_of_range);
check<float>("1e-99999999999999999999", 0.0f, 23, std::errc::result_out_of_range);
check<float>("1e+99999999999999999999", std::numeric_limits<float>::infinity(), 23, std::errc::result_out_of_range);
check<float>("-1e-99999999999999999999", -0.0f, 24, std::errc::result_out_of_range);
check<float>("-1e+99999999999999999999", -std::numeric_limits<float>::infinity(), 24, std::errc::result_out_of_range);
}
SECTION("double precision")
{
check<double>("", init_val, 0, std::errc::invalid_argument);
check<double>(" 123", init_val, 0, std::errc::invalid_argument);
check<double>("123 ", 123.0, 3, std::errc{});
check<double>("+123", init_val, 0, std::errc::invalid_argument);
check<double>("-123", -123.0, 4, std::errc{});
check<double>("123", 123.0, 3, std::errc{});
check<double>("123e10", 123e10, 6, std::errc{});
check<double>("123e+10", 123e10, 7, std::errc{});
check<double>("123e-10", 123e-10, 7, std::errc{});
check<double>("123.456", 123.456, 7, std::errc{});
check<double>("123;456", 123.0, 3, std::errc{});
check<double>("123.456 ", 123.456, 7, std::errc{});
check<double>("123;456 ", 123.0, 3, std::errc{});
check<double>("123456789.123456789", 123456789.123456789, 19, std::errc{});
check<double>("1e40", 1e40, 4, std::errc{});
check<double>("-1e40", -1e40, 5, std::errc{});
check<double>("2e308", std::numeric_limits<double>::infinity(), 5, std::errc::result_out_of_range);
check<double>("-2e308", -std::numeric_limits<double>::infinity(), 6, std::errc::result_out_of_range);
check<double>("2e-324", 0.0, 6, std::errc::result_out_of_range);
check<double>("-2e-324", -0.0, 7, std::errc::result_out_of_range);
check<double>("123.456e-789", 0.0, 12, std::errc::result_out_of_range);
check<double>("-123.456e-789", -0.0, 13, std::errc::result_out_of_range);
check<double>("1e-99999999999999999999", 0.0, 23, std::errc::result_out_of_range);
check<double>("1e+99999999999999999999", std::numeric_limits<double>::infinity(), 23, std::errc::result_out_of_range);
check<double>("-1e-99999999999999999999", -0.0, 24, std::errc::result_out_of_range);
check<double>("-1e+99999999999999999999", -std::numeric_limits<double>::infinity(), 24, std::errc::result_out_of_range);
}
SECTION("long double precision")
{
check<long double>("", init_val, 0, std::errc::invalid_argument);
check<long double>(" 123", init_val, 0, std::errc::invalid_argument);
check<long double>("123 ", 123.0L, 3, std::errc{});
check<long double>("+123", init_val, 0, std::errc::invalid_argument);
check<long double>("-123", -123.0L, 4, std::errc{});
check<long double>("123", 123.0L, 3, std::errc{});
check<long double>("123e10", 123e10L, 6, std::errc{});
check<long double>("123e+10", 123e10L, 7, std::errc{});
check<long double>("123e-10", 123e-10L, 7, std::errc{});
check<long double>("123.456", 123.456L, 7, std::errc{});
check<long double>("123;456", 123.0L, 3, std::errc{});
check<long double>("123.456 ", 123.456L, 7, std::errc{});
check<long double>("123;456 ", 123.0L, 3, std::errc{});
check<long double>("123456789.123456789", 123456789.123456789L, 19, std::errc{});
check<long double>("1e40", 1e40L, 4, std::errc{});
check<long double>("-1e40", -1e40L, 5, std::errc{});
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable: 4127)
#endif
if (sizeof(long double) > 8)
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
{
// Right-hand-side is calculated to avoid warning about literal
// exceeding range on platforms where this branch is NOT taken.
check<long double>("2e308", 1e308L * 2, 5, std::errc{});
check<long double>("-2e308", -1e308L * 2, 6, std::errc{});
check<long double>("2e-324", 8e-324L / 4, 6, std::errc{});
check<long double>("-2e-324", -8e-324L / 4, 7, std::errc{});
}
else
{
check<long double>("2e308", std::numeric_limits<long double>::infinity(), 5, std::errc::result_out_of_range);
check<long double>("-2e308", -std::numeric_limits<long double>::infinity(), 6, std::errc::result_out_of_range);
check<long double>("2e-324", 0.0L, 6, std::errc::result_out_of_range);
check<long double>("-2e-324", -0.0L, 7, std::errc::result_out_of_range);
}
check<long double>("1e5000", std::numeric_limits<long double>::infinity(), 6, std::errc::result_out_of_range);
check<long double>("-1e5000", -std::numeric_limits<long double>::infinity(), 7, std::errc::result_out_of_range);
check<long double>("123.456e-7890", 0.0L, 13, std::errc::result_out_of_range);
check<long double>("-123.456e-7890", -0.0L, 14, std::errc::result_out_of_range);
check<long double>("1e-99999999999999999999", 0.0L, 23, std::errc::result_out_of_range);
check<long double>("1e+99999999999999999999", std::numeric_limits<long double>::infinity(), 23, std::errc::result_out_of_range);
check<long double>("-1e-99999999999999999999", -0.0L, 24, std::errc::result_out_of_range);
check<long double>("-1e+99999999999999999999", -std::numeric_limits<long double>::infinity(), 24, std::errc::result_out_of_range);
}
}