diff --git a/include/behaviortree_cpp/utils/convert_impl.hpp b/include/behaviortree_cpp/utils/convert_impl.hpp index 2a9f7c34c..e32eefbcf 100644 --- a/include/behaviortree_cpp/utils/convert_impl.hpp +++ b/include/behaviortree_cpp/utils/convert_impl.hpp @@ -114,8 +114,15 @@ inline void checkTruncation(const From& from) // Handle floating point to integer else if constexpr(std::is_floating_point_v && std::is_integral_v) { - if(from > static_cast(std::numeric_limits::max()) || - from < static_cast(std::numeric_limits::lowest()) || + // static_cast(To::max) rounds up to an out-of-range value when To has + // more significant bits than From's mantissa (e.g. double -> int64_t). In + // that case a plain `>` lets the rounded boundary through and the cast to To + // below overflows, so use a half-open upper bound to reject it first. + constexpr bool max_rounds_up = + std::numeric_limits::digits > std::numeric_limits::digits; + const From max_limit = static_cast(std::numeric_limits::max()); + const bool over = max_rounds_up ? (from >= max_limit) : (from > max_limit); + if(over || from < static_cast(std::numeric_limits::lowest()) || from != std::nearbyint(from)) { throw std::runtime_error("Invalid floating point to integer conversion"); diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 41ce7c350..4a0708896 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -259,8 +259,16 @@ inline bool ValidCast(const SRC& val) // Handle conversion to integral else if constexpr(std::is_integral_v) { - if(val > static_cast(std::numeric_limits::max()) || - val < static_cast(std::numeric_limits::lowest())) + // static_cast(TO::max) rounds up to an out-of-range value when TO has + // more significant bits than a floating SRC's mantissa (e.g. double -> + // int64_t). A plain `>` would let that rounded boundary through and the + // cast to TO below overflows, so use a half-open upper bound in that case. + constexpr bool max_rounds_up = + std::is_floating_point_v && + std::numeric_limits::digits > std::numeric_limits::digits; + const SRC max_limit = static_cast(std::numeric_limits::max()); + const bool over = max_rounds_up ? (val >= max_limit) : (val > max_limit); + if(over || val < static_cast(std::numeric_limits::lowest())) { return false; } diff --git a/tests/gtest_basic_types.cpp b/tests/gtest_basic_types.cpp index 57f8bafca..680c45ce3 100644 --- a/tests/gtest_basic_types.cpp +++ b/tests/gtest_basic_types.cpp @@ -123,6 +123,31 @@ TEST(BasicTypes, ConvertFromString_Double) ASSERT_THROW((void)convertFromString("not_a_number"), RuntimeError); } +TEST(BasicTypes, DoubleToIntBoundary) +{ + // A double that lands exactly on 2^63 must be rejected before the cast, not + // static_cast to int64_t (which is out of range and undefined behavior). + // 2^63 = 9223372036854775808.0; INT64_MAX is 9223372036854775807. + const double two_pow_63 = 9223372036854775808.0; + EXPECT_ANY_THROW((void)BT::Any(two_pow_63).cast()); + + // 2^64 lands one past UINT64_MAX and must likewise be rejected. + const double two_pow_64 = 18446744073709551616.0; + EXPECT_ANY_THROW((void)BT::Any(two_pow_64).cast()); + + // In-range integral doubles still convert cleanly. + const double in_range = 4611686018427387904.0; // 2^62 + EXPECT_EQ(BT::Any(in_range).cast(), int64_t(4611686018427387904LL)); + EXPECT_EQ(BT::Any(1000.0).cast(), int64_t(1000)); + + // Same boundary reached through Blackboard::set, which routes the numeric + // safe-cast check (isCastingSafe/ValidCast) instead of Any::convert. + auto bb = BT::Blackboard::create(); + bb->set("k", int64_t(1)); // lock the entry to int64_t + EXPECT_ANY_THROW(bb->set("k", two_pow_63)); + EXPECT_NO_THROW(bb->set("k", in_range)); +} + TEST(BasicTypes, ConvertFromString_Bool) { ASSERT_TRUE(convertFromString("true"));