Skip to content

Commit cf2f0d4

Browse files
authored
Merge pull request #392 from cppalliance/overflow
Fix possible overflow in mini_from_chars
2 parents c741952 + 154acf7 commit cf2f0d4

3 files changed

Lines changed: 284 additions & 32 deletions

File tree

include/boost/int128/detail/mini_from_chars.hpp

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ BOOST_INT128_HOST_DEVICE constexpr int from_chars_integer_impl(const char* first
127127

128128

129129
overflow_value /= unsigned_base;
130-
overflow_value <<= 1;
131130
max_digit %= unsigned_base;
132131

133132
// If the only character was a sign abort now
@@ -138,48 +137,52 @@ BOOST_INT128_HOST_DEVICE constexpr int from_chars_integer_impl(const char* first
138137

139138
bool overflowed = false;
140139

141-
std::ptrdiff_t nc = last - next;
142-
constexpr std::ptrdiff_t nd = std::numeric_limits<Integer>::digits10;
140+
const std::ptrdiff_t nc = last - next;
143141

142+
// For bases 2..10 the first digits10 characters always fit in the unsigned
143+
// For bases above 10, the safe window is shorter, so we must check with each iteration
144+
const std::ptrdiff_t nd {
145+
base <= 10
146+
? static_cast<std::ptrdiff_t>(std::numeric_limits<Integer>::digits10)
147+
: std::ptrdiff_t{0}
148+
};
149+
150+
const std::ptrdiff_t fast_limit {nd < nc ? nd : nc};
151+
std::ptrdiff_t i = 0;
152+
153+
for (; i < fast_limit; ++i)
144154
{
145-
std::ptrdiff_t i = 0;
155+
const auto current_digit = static_cast<Unsigned_Integer>(digit_from_char(*next));
146156

147-
for( ; i < nd && i < nc; ++i )
157+
if (current_digit >= unsigned_base)
148158
{
149-
// overflow is not possible in the first nd characters
159+
break;
160+
}
150161

151-
const auto current_digit = static_cast<Unsigned_Integer>(digit_from_char(*next));
162+
result = static_cast<Unsigned_Integer>(result * unsigned_base + current_digit);
163+
++next;
164+
}
152165

153-
if (current_digit >= unsigned_base)
154-
{
155-
break;
156-
}
166+
for (; i < nc; ++i)
167+
{
168+
const auto current_digit = static_cast<Unsigned_Integer>(digit_from_char(*next));
157169

158-
result = static_cast<Unsigned_Integer>(result * unsigned_base + current_digit);
159-
++next;
170+
if (current_digit >= unsigned_base)
171+
{
172+
break;
160173
}
161174

162-
for( ; i < nc; ++i )
175+
if (result < overflow_value || (result == overflow_value && current_digit <= max_digit))
163176
{
164-
const auto current_digit = static_cast<Unsigned_Integer>(digit_from_char(*next));
165-
166-
if (current_digit >= unsigned_base)
167-
{
168-
break;
169-
}
170-
171-
if (result < overflow_value || (result == overflow_value && current_digit <= max_digit))
172-
{
173-
result = static_cast<Unsigned_Integer>(result * unsigned_base + current_digit);
174-
}
175-
else
176-
{
177-
overflowed = true;
178-
break;
179-
}
180-
181-
++next;
177+
result = static_cast<Unsigned_Integer>(result * unsigned_base + current_digit);
182178
}
179+
else
180+
{
181+
overflowed = true;
182+
break;
183+
}
184+
185+
++next;
183186
}
184187

185188
// Return the parsed value, adding the sign back if applicable

test/Jamfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ run test_climits.cpp ;
6565

6666
run test_bit.cpp ;
6767
run test_literals.cpp ;
68+
run test_from_chars_bases.cpp ;
6869
run test_stream.cpp ;
6970

7071
run test_mixed_type_sign_compare.cpp ;

test/test_from_chars_bases.cpp

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright 2026 Matt Borland
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// https://www.boost.org/LICENSE_1_0.txt
4+
5+
// Exercises boost::int128::detail::from_chars across every base it supports
6+
// (2..36) for both int128_t and uint128_t. Locks in the fix to the per-iteration
7+
// overflow threshold (the spurious overflow_value <<= 1 was masking MAX+1
8+
// overflow in mini_from_chars).
9+
10+
#ifndef BOOST_INT128_BUILD_MODULE
11+
12+
#include <boost/int128/int128.hpp>
13+
#include <boost/int128/detail/mini_from_chars.hpp>
14+
15+
#else
16+
17+
import boost.int128;
18+
19+
#endif
20+
21+
#include <boost/core/lightweight_test.hpp>
22+
23+
#include <algorithm>
24+
#include <cerrno>
25+
#include <limits>
26+
#include <string>
27+
28+
namespace {
29+
30+
using boost::int128::int128_t;
31+
using boost::int128::uint128_t;
32+
33+
// Format a uint128_t into a base-N string (lowercase). Self-contained so the
34+
// test does not pull in boost::charconv just to generate inputs.
35+
std::string format_unsigned(uint128_t value, int base)
36+
{
37+
if (value == uint128_t{0U})
38+
{
39+
return "0";
40+
}
41+
42+
const auto ubase {static_cast<unsigned>(base)};
43+
std::string out;
44+
while (value != uint128_t{0U})
45+
{
46+
const auto digit {static_cast<unsigned>(value % ubase)};
47+
const char c {digit < 10U ? static_cast<char>('0' + digit)
48+
: static_cast<char>('a' + digit - 10U)};
49+
out.push_back(c);
50+
value /= ubase;
51+
}
52+
std::reverse(out.begin(), out.end());
53+
return out;
54+
}
55+
56+
std::string format_signed(int128_t value, int base)
57+
{
58+
if (value == int128_t{0})
59+
{
60+
return "0";
61+
}
62+
63+
if (value == (std::numeric_limits<int128_t>::min)())
64+
{
65+
// |INT128_MIN| does not fit in int128_t; do the magnitude in uint128_t.
66+
const uint128_t magnitude {uint128_t{1} << 127U};
67+
std::string out {format_unsigned(magnitude, base)};
68+
out.insert(out.begin(), '-');
69+
return out;
70+
}
71+
72+
if (value < int128_t{0})
73+
{
74+
std::string out {format_unsigned(static_cast<uint128_t>(-value), base)};
75+
out.insert(out.begin(), '-');
76+
return out;
77+
}
78+
79+
return format_unsigned(static_cast<uint128_t>(value), base);
80+
}
81+
82+
inline std::string format_value(int128_t value, int base)
83+
{
84+
return format_signed(value, base);
85+
}
86+
87+
inline std::string format_value(uint128_t value, int base)
88+
{
89+
return format_unsigned(value, base);
90+
}
91+
92+
template <typename T>
93+
void check_roundtrip(T expected, int base)
94+
{
95+
const std::string s {format_value(expected, base)};
96+
97+
T parsed {};
98+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), parsed, base)};
99+
100+
BOOST_TEST_LT(r, 0);
101+
BOOST_TEST(parsed == expected);
102+
}
103+
104+
template <typename T>
105+
void check_overflow(const std::string& s, int base)
106+
{
107+
T parsed {};
108+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), parsed, base)};
109+
110+
BOOST_TEST_EQ(r, EDOM);
111+
}
112+
113+
void test_uint128_all_bases()
114+
{
115+
constexpr auto max_value {(std::numeric_limits<uint128_t>::max)()};
116+
117+
for (int base {2}; base <= 36; ++base)
118+
{
119+
// Canonical small values.
120+
check_roundtrip<uint128_t>(uint128_t{0U}, base);
121+
check_roundtrip<uint128_t>(uint128_t{1U}, base);
122+
check_roundtrip<uint128_t>(static_cast<uint128_t>(static_cast<unsigned>(base) - 1U), base);
123+
check_roundtrip<uint128_t>(static_cast<uint128_t>(static_cast<unsigned>(base)), base);
124+
125+
// A handful of mid-range values that span the per-base digit window.
126+
check_roundtrip<uint128_t>(uint128_t{42U}, base);
127+
check_roundtrip<uint128_t>(uint128_t{1234567890U}, base);
128+
check_roundtrip<uint128_t>(uint128_t{0xFFFFFFFFFFFFFFFFULL}, base);
129+
check_roundtrip<uint128_t>(uint128_t{1U} << 100U, base);
130+
131+
// The boundary itself parses correctly.
132+
check_roundtrip<uint128_t>(max_value, base);
133+
134+
// MAX with any extra digit appended is at least MAX * base, which
135+
// overflows uint128_t for every base in [2, 36].
136+
const auto max_str {format_unsigned(max_value, base)};
137+
check_overflow<uint128_t>(max_str + "0", base);
138+
}
139+
}
140+
141+
void test_int128_all_bases()
142+
{
143+
constexpr auto max_value {(std::numeric_limits<int128_t>::max)()};
144+
constexpr auto min_value {(std::numeric_limits<int128_t>::min)()};
145+
146+
for (int base {2}; base <= 36; ++base)
147+
{
148+
check_roundtrip<int128_t>(int128_t{0}, base);
149+
check_roundtrip<int128_t>(int128_t{1}, base);
150+
check_roundtrip<int128_t>(int128_t{-1}, base);
151+
check_roundtrip<int128_t>(int128_t{42}, base);
152+
check_roundtrip<int128_t>(int128_t{-42}, base);
153+
check_roundtrip<int128_t>(int128_t{1234567890}, base);
154+
check_roundtrip<int128_t>(int128_t{-1234567890}, base);
155+
156+
// Both signed boundaries parse correctly.
157+
check_roundtrip<int128_t>(max_value, base);
158+
check_roundtrip<int128_t>(min_value, base);
159+
160+
// Append a digit to push past the magnitude bound on each side.
161+
const auto max_str {format_signed(max_value, base)};
162+
check_overflow<int128_t>(max_str + "0", base);
163+
164+
const auto min_str {format_signed(min_value, base)};
165+
check_overflow<int128_t>(min_str + "0", base);
166+
}
167+
}
168+
169+
void test_decimal_boundaries()
170+
{
171+
// Tight base-10 boundary cases: the spurious <<= 1 in the threshold made
172+
// these silently produce wrong values instead of returning EDOM.
173+
174+
// UINT128_MAX exactly.
175+
{
176+
const std::string s {"340282366920938463463374607431768211455"};
177+
uint128_t v {};
178+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)};
179+
BOOST_TEST_LT(r, 0);
180+
BOOST_TEST(v == (std::numeric_limits<uint128_t>::max)());
181+
}
182+
183+
// UINT128_MAX + 1.
184+
check_overflow<uint128_t>("340282366920938463463374607431768211456", 10);
185+
186+
// INT128_MAX exactly.
187+
{
188+
const std::string s {"170141183460469231731687303715884105727"};
189+
int128_t v {};
190+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)};
191+
BOOST_TEST_LT(r, 0);
192+
BOOST_TEST(v == (std::numeric_limits<int128_t>::max)());
193+
}
194+
195+
// INT128_MAX + 1.
196+
check_overflow<int128_t>("170141183460469231731687303715884105728", 10);
197+
198+
// INT128_MIN exactly.
199+
{
200+
const std::string s {"-170141183460469231731687303715884105728"};
201+
int128_t v {};
202+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)};
203+
BOOST_TEST_LT(r, 0);
204+
BOOST_TEST(v == (std::numeric_limits<int128_t>::min)());
205+
}
206+
207+
// INT128_MIN - 1.
208+
check_overflow<int128_t>("-170141183460469231731687303715884105729", 10);
209+
}
210+
211+
void test_invalid_inputs()
212+
{
213+
// Empty range is EINVAL.
214+
{
215+
const char* s {""};
216+
uint128_t v {};
217+
const auto r {boost::int128::detail::from_chars(s, s, v)};
218+
BOOST_TEST_EQ(r, EINVAL);
219+
}
220+
221+
// Lone sign is EINVAL.
222+
{
223+
const std::string s {"-"};
224+
int128_t v {};
225+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)};
226+
BOOST_TEST_EQ(r, EINVAL);
227+
}
228+
229+
// Leading sign on the unsigned overload is EINVAL.
230+
{
231+
const std::string s {"-1"};
232+
uint128_t v {};
233+
const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)};
234+
BOOST_TEST_EQ(r, EINVAL);
235+
}
236+
}
237+
238+
} // anonymous namespace
239+
240+
int main()
241+
{
242+
test_uint128_all_bases();
243+
test_int128_all_bases();
244+
test_decimal_boundaries();
245+
test_invalid_inputs();
246+
247+
return boost::report_errors();
248+
}

0 commit comments

Comments
 (0)