Skip to content

Commit 6a3b337

Browse files
check from_chars result for integer literals in script parser (#1214)
1 parent ae44006 commit 6a3b337

2 files changed

Lines changed: 35 additions & 7 deletions

File tree

‎src/script_parser.cpp‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,19 @@ class ScriptParser
218218
int64_t val = 0;
219219
const char* first = tok.text.data();
220220
const char* last = first + tok.text.size();
221-
if(tok.text.size() > 2 && tok.text[0] == '0' &&
222-
(tok.text[1] == 'x' || tok.text[1] == 'X'))
221+
const bool is_hex = tok.text.size() > 2 && tok.text[0] == '0' &&
222+
(tok.text[1] == 'x' || tok.text[1] == 'X');
223+
const auto [ptr, ec] = is_hex ? std::from_chars(first + 2, last, val, 16) :
224+
std::from_chars(first, last, val, 10);
225+
// The tokenizer already guarantees the literal is made of (hex) digits, so
226+
// the only remaining failure is result_out_of_range. from_chars leaves val
227+
// untouched in that case, which would otherwise turn a literal like
228+
// 0xFFFFFFFFFFFFFFFF into a silent 0.
229+
if(ec != std::errc() || ptr != last)
223230
{
224-
std::from_chars(first + 2, last, val, 16);
225-
}
226-
else
227-
{
228-
std::from_chars(first, last, val, 10);
231+
throw RuntimeError(StrCat("Integer literal '", tok.text, "' at position ",
232+
std::to_string(tok.pos),
233+
" is out of range for a 64-bit integer"));
229234
}
230235
return std::make_shared<Ast::ExprLiteral>(Any(val));
231236
}

‎tests/script_parser_test.cpp‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ TEST(ParserTest, AnyTypes_Failing)
9898
EXPECT_FALSE(BT::ParseScriptAndExecute(env, "foo").has_value());
9999
}
100100

101+
TEST(ParserTest, IntegerLiteralOutOfRangeIsRejected)
102+
{
103+
BT::Ast::Environment env = { BT::Blackboard::create(), {} };
104+
105+
// These literals overflow int64_t. std::from_chars leaves its output untouched
106+
// on result_out_of_range, so the parser used to accept them and silently
107+
// evaluate the literal as 0 (e.g. the all-bits mask 0xFFFFFFFFFFFFFFFF).
108+
EXPECT_FALSE(BT::ValidateScript("9223372036854775808")); // INT64_MAX + 1
109+
EXPECT_FALSE(BT::ValidateScript("99999999999999999999999"));
110+
EXPECT_FALSE(BT::ValidateScript("0xFFFFFFFFFFFFFFFF"));
111+
EXPECT_FALSE(BT::ValidateScript("0x8000000000000000"));
112+
EXPECT_FALSE(BT::ParseScriptAndExecute(env, "0xFFFFFFFFFFFFFFFF").has_value());
113+
114+
// The boundary values that do fit must still parse and keep their value.
115+
auto max_val = BT::ParseScriptAndExecute(env, "9223372036854775807");
116+
ASSERT_TRUE(max_val.has_value());
117+
EXPECT_EQ(max_val.value().cast<int64_t>(), 9223372036854775807LL);
118+
119+
auto hex_val = BT::ParseScriptAndExecute(env, "0x7FFFFFFFFFFFFFFF");
120+
ASSERT_TRUE(hex_val.has_value());
121+
EXPECT_EQ(hex_val.value().cast<int64_t>(), 9223372036854775807LL);
122+
}
123+
101124
TEST(ParserTest, DeeplyNestedScriptIsRejected)
102125
{
103126
// A script with thousands of nested parentheses or unary prefixes used to add

0 commit comments

Comments
 (0)