detect uint64 overflow that wraps past min_safe in parse_int_string

This commit is contained in:
sahvx655-wq 2026-06-13 21:20:45 +05:30 committed by Daniel Lemire
parent 8234a89623
commit 632cc97b5b
2 changed files with 77 additions and 3 deletions

View File

@ -781,9 +781,28 @@ parse_int_string(UC const *p, UC const *pend, T &value,
}
// this check can be eliminated for all other types, but they will all require
// a max_digits(base) equivalent
if (digit_count == max_digits && i < min_safe_u64(base)) {
answer.ec = std::errc::result_out_of_range;
return answer;
if (digit_count == max_digits) {
// A value that wrapped below the smallest max_digits-length value has
// certainly overflowed.
if (i < min_safe_u64(base)) {
answer.ec = std::errc::result_out_of_range;
return answer;
}
// i >= min_safe_u64(base) is still not proof that it fits: for any base
// whose max_digits-length range exceeds 2^64 (base 10 reaches ~5.4 * 2^64
// at 20 digits) the accumulator can wrap a whole multiple of 2^64 and land
// back above min_safe, so the test above lets that overflow through. Re-run
// the parsed digits with a checked multiply-add to decide exactly.
uint64_t overflow_check = 0;
for (UC const *q = start_digits; q != p; ++q) {
uint8_t const digit = ch_to_digit(*q);
if (overflow_check >
(std::numeric_limits<uint64_t>::max() - digit) / uint64_t(base)) {
answer.ec = std::errc::result_out_of_range;
return answer;
}
overflow_check = uint64_t(base) * overflow_check + digit;
}
}
// check other types overflow

View File

@ -821,6 +821,61 @@ int main() {
++base_unsigned;
}
// unsigned out of range error base test, multi-wrap (64 bit)
// These values overflow uint64_t, but the accumulator wraps a whole multiple
// of 2^64 and lands back at or above the smallest max_digits-length value, so
// a single comparison against that bound does not catch the overflow. Bases
// 2, 4 and 16 are excluded because their max_digits-length range fits within
// a single 2^64 span.
std::vector<int> const unsigned_multiwrap_base{
3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36};
std::vector<std::string_view> const unsigned_multiwrap_base_test{
"22222222222222222222222222222222222222222",
"4400000000000000000000000000",
"5555555555555555555555555",
"66666666666666666666666",
"7777777777777777777777",
"888888888888888888888",
"46893488147419103233",
"AAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBB",
"427772311192C9BAAB",
"DDDDDDDDDDDDDDDDD",
"532C82996D3A44919",
"GGGGGGGGGGGGGGGG",
"HHHHHHHHHHHHHHHH",
"3835GEGDF36622EG",
"JJJJJJJJJJJJJJJ",
"KKKKKKKKKKKKKKK",
"LLLLLLLLLLLLLLL",
"444BGHB4EG5DA2D",
"NNNNNNNNNNNNNN",
"JE5H4MNDLJGNLO",
"PPPPPPPPPPPPPP",
"QQQQQQQQQQQQQQ",
"RRRRRRRRRRRRRR",
"4H7QS52310IHQK",
"TTTTTTTTTTTTTT",
"UUUUUUUUUUUUU",
"VVVVVVVVVVVVV",
"WWWWWWWWWWWWW",
"XXXXXXXXXXXXX",
"YYYYYYYYYYYYY",
"6U831JL976P6O"};
for (std::size_t i = 0; i < unsigned_multiwrap_base_test.size(); ++i) {
auto const &f = unsigned_multiwrap_base_test[i];
uint64_t result;
auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result,
unsigned_multiwrap_base[i]);
if (answer.ec != std::errc::result_out_of_range) {
std::cerr << "expected error for should be 'result_out_of_range': \"" << f
<< "\"" << std::endl;
return EXIT_FAILURE;
}
}
// just within range base test (64 bit)
std::vector<std::string_view> const int_within_range_base_test{
"111111111111111111111111111111111111111111111111111111111111111",