diff --git a/fuzz/from_chars.cc b/fuzz/from_chars.cc index 35fe9fe..c6855af 100644 --- a/fuzz/from_chars.cc +++ b/fuzz/from_chars.cc @@ -1,38 +1,205 @@ #include "fast_float/fast_float.h" #include +#include +#include +#include +#include #include #include +#include +#include + +#if defined(__has_include) +#if __has_include() && __cplusplus >= 201703L +#include +#define FASTFLOAT_FUZZ_CHARCONV 1 +#endif +#endif + +static void fail(char const *check) { + fprintf(stderr, "fast_float fuzz failure: %s\n", check); + abort(); +} fast_float::chars_format arbitrary_format(FuzzedDataProvider &fdp) { using fast_float::chars_format; - switch (fdp.ConsumeIntegralInRange(0, 3)) { + chars_format format; + switch (fdp.ConsumeIntegralInRange(0, 7)) { case 0: - return chars_format::scientific; + format = chars_format::scientific; break; case 1: - return chars_format::fixed; + format = chars_format::fixed; break; case 2: - return chars_format::fixed; + format = chars_format::json; + break; + case 3: + format = chars_format::json_or_infnan; + break; + case 4: + format = chars_format::fortran; + break; + case 5: + format = chars_format::javascript; + break; + case 6: + format = chars_format::javascript_sloppy; + break; + default: + format = chars_format::general; break; } - return chars_format::general; + if (fdp.ConsumeBool()) { + format = format | chars_format::allow_leading_plus; + } + if (fdp.ConsumeBool()) { + format = format | chars_format::skip_white_space; + } + return format; +} + +template uint64_t bits_of(T value) { + uint64_t bits = 0; + memcpy(&bits, &value, sizeof(T)); + return bits; +} + +// The same ASCII text must parse identically for every character type: same +// error code, same number of consumed code units, bit-identical value. +template +void check_widened(std::string const &input, + fast_float::from_chars_result_t expected, + double expected_value, fast_float::chars_format format) { + std::vector wide; + wide.reserve(input.size()); + for (char c : input) { + wide.push_back(UC(static_cast(c))); + } + double value = 0.0; + auto result = fast_float::from_chars(wide.data(), wide.data() + wide.size(), + value, format); + if (result.ec != expected.ec || + (result.ptr - wide.data()) != (expected.ptr - input.data())) { + fail("wide result disagrees with char result"); + } + if (expected.ec == std::errc() && bits_of(value) != bits_of(expected_value)) { + fail("wide value disagrees with char value"); + } +} + +// Every number format accepted by the library is pure ASCII, so a successful +// parse must never consume a code unit >= 0x80. A violation means some helper +// inspected only the low byte of a wide code unit (the bug behind #391). +// The generated sequences keep an ASCII low byte but take hostile high bits: +// top-bit-set units (near the minimum of a signed wchar_t, the shape behind +// #422), sign-extended negative units (the shape behind #395), and non-ASCII +// code points whose low byte is an ASCII digit (the shape behind #391). +template +void check_arbitrary_units(FuzzedDataProvider &fdp, + fast_float::chars_format format) { + using unsigned_uc = typename std::make_unsigned::type; + size_t count = fdp.ConsumeIntegralInRange(0, 64); + std::vector units; + units.reserve(count); + for (size_t i = 0; i < count; ++i) { + unsigned_uc unit = fdp.ConsumeIntegral(); + switch (fdp.ConsumeIntegralInRange(0, 4)) { + case 0: // top bit set: near the minimum value when UC is signed + unit = unsigned_uc(unit | (unsigned_uc(1) << (sizeof(UC) * 8 - 1))); + break; + case 1: // all high bits set: a small negative value when UC is signed + unit = unsigned_uc(unit | ~unsigned_uc(0xFF)); + break; + case 2: // arbitrary high byte: a non-ASCII code point, ASCII low byte + unit = unsigned_uc( + unit | unsigned_uc(unsigned_uc(fdp.ConsumeIntegral()) << 8)); + break; + default: // plain ASCII unit + break; + } + units.push_back(UC(unit)); + } + if (units.empty()) { + return; + } + UC const *begin = units.data(); + UC const *end = begin + units.size(); + + double as_double = 0.0; + auto result = fast_float::from_chars(begin, end, as_double, format); + if (result.ec == std::errc()) { + for (UC const *p = begin; p != result.ptr; ++p) { + if (static_cast(*p) >= 0x80) { + fail("float parse consumed a non-ASCII code unit"); + } + } + } + uint16_t as_uint16 = 0; + auto int_result = fast_float::from_chars(begin, end, as_uint16); + if (int_result.ec == std::errc()) { + for (UC const *p = begin; p != int_result.ptr; ++p) { + if (static_cast(*p) >= 0x80) { + fail("integer parse consumed a non-ASCII code unit"); + } + } + } +} + +// fast_float's integer parsing is specified to match std::from_chars. +template void check_int(std::string const &input, int base) { + T value{}; + auto result = fast_float::from_chars( + input.data(), input.data() + input.size(), value, base); +#ifdef FASTFLOAT_FUZZ_CHARCONV + T expected{}; + auto std_result = std::from_chars(input.data(), input.data() + input.size(), + expected, base); + if (result.ec != std_result.ec || result.ptr != std_result.ptr) { + fail("integer result disagrees with std::from_chars"); + } + if (result.ec == std::errc() && value != expected) { + fail("integer value disagrees with std::from_chars"); + } +#else + static_cast(result); +#endif +} + +template +void check_int_all_bases(std::string const &input, int base) { + check_int(input, 10); + check_int(input, base); } extern "C" int LLVMFuzzerTestOneInput(uint8_t const *data, size_t size) { FuzzedDataProvider fdp(data, size); fast_float::chars_format format = arbitrary_format(fdp); - double result_d = 0.0; + int base = fdp.ConsumeIntegralInRange(2, 36); + std::string input_d = fdp.ConsumeRandomLengthString(128); - auto answer = fast_float::from_chars( + double result_d = 0.0; + auto answer_d = fast_float::from_chars( input_d.data(), input_d.data() + input_d.size(), result_d, format); - std::string input_f = fdp.ConsumeRandomLengthString(128); float result_f = 0.0; - answer = fast_float::from_chars( - input_f.data(), input_f.data() + input_f.size(), result_f, format); - int result_i = 0; + fast_float::from_chars(input_d.data(), input_d.data() + input_d.size(), + result_f, format); + check_widened(input_d, answer_d, result_d, format); + check_widened(input_d, answer_d, result_d, format); + check_widened(input_d, answer_d, result_d, format); + std::string input_i = fdp.ConsumeRandomLengthString(128); - answer = fast_float::from_chars(input_i.data(), - input_i.data() + input_i.size(), result_i); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + check_int_all_bases(input_i, base); + + check_arbitrary_units(fdp, format); + check_arbitrary_units(fdp, format); + check_arbitrary_units(fdp, format); return 0; -} \ No newline at end of file +} diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 5ed0181..a23b8c2 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -248,7 +248,7 @@ clinger_fast_path_impl(am_mant_t const mantissa, am_pow_t const exponent, // Only std::float16_t can overflow here (e.g., "656e2"); let the // slow path report result_out_of_range. if (binary_format::fast_path_can_overflow() && - value > (std::numeric_limits::max)()) { + value > std::numeric_limits::max()) { return false; } } @@ -590,7 +590,7 @@ FASTFLOAT_CONSTEXPR20 const auto is_negative = mantissa < 0; // Negate as unsigned: -mantissa overflows for the most negative int64_t. const auto m = is_negative ? am_mant_t(0) - static_cast(mantissa) - : static_cast(mantissa); + : static_cast(mantissa); #else FASTFLOAT_ASSUME(mantissa >= 0); const auto m = static_cast(mantissa); diff --git a/tests/fixedwidthtest.cpp b/tests/fixedwidthtest.cpp index 510d2be..e7dabe7 100644 --- a/tests/fixedwidthtest.cpp +++ b/tests/fixedwidthtest.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "fast_float/fast_float.h" #include @@ -9,6 +11,39 @@ #include #endif +// If no characters match the pattern, value must be left unmodified +// (see https://github.com/fastfloat/fast_float/issues/424). +template bool check_invalid_input_preserves_value() { + std::vector const invalid_inputs{"x", "", "-", ".", "e1", + "-x", "na", "+"}; + for (auto const &f : invalid_inputs) { + T result = T(42); + auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result); + if (answer.ec != std::errc::invalid_argument || answer.ptr != f.data()) { + std::cerr << "Expected invalid_argument for input: \"" << f << "\"" + << std::endl; + return false; + } + if (result != T(42)) { + std::cerr << "Value modified on invalid input: \"" << f << "\" got " + << result << std::endl; + return false; + } + } + // Out-of-range input must still store the documented value. + std::string_view const too_large = "1e999"; + T result = T(42); + auto answer = fast_float::from_chars( + too_large.data(), too_large.data() + too_large.size(), result); + if (answer.ec != std::errc::result_out_of_range || + result != std::numeric_limits::infinity()) { + std::cerr << "Expected infinity and result_out_of_range for \"1e999\"" + << std::endl; + return false; + } + return true; +} + int main() { // Write some testcases for the parsing of floating point numbers in the // float32_t type. We use the from_chars function defined in this library. @@ -33,6 +68,9 @@ int main() { return EXIT_FAILURE; } } + if (!check_invalid_input_preserves_value()) { + return EXIT_FAILURE; + } #else std::cout << "No std::float32_t type available." << std::endl; #endif @@ -60,6 +98,9 @@ int main() { return EXIT_FAILURE; } } + if (!check_invalid_input_preserves_value()) { + return EXIT_FAILURE; + } #else std::cout << "No std::float64_t type available." << std::endl; #endif