diff --git a/src/util/narrow.h b/src/util/narrow.h index 02573387262..416b370c889 100644 --- a/src/util/narrow.h +++ b/src/util/narrow.h @@ -38,4 +38,17 @@ output_type narrow(input_type input) return output; } +/// Run-time checked narrow cast. Throws a std::out_of_range error +/// if the input cannot be converted to the output_type without data lass +template +output_type narrow_or_throw_out_of_range(input_type input) +{ + auto const result = narrow_cast(input); + if(result != input) + { + throw std::out_of_range{"narrowing gave a different value than expected"}; + } + return result; +} + #endif // CPROVER_UTIL_NARROW_H diff --git a/src/util/string2int.cpp b/src/util/string2int.cpp index 2285c3e0691..fbab23f0c08 100644 --- a/src/util/string2int.cpp +++ b/src/util/string2int.cpp @@ -10,82 +10,66 @@ Author: Michael Tautschnig, michael.tautschnig@cs.ox.ac.uk #include #include +#include #include +#include #include "invariant.h" -template -inline T str2number(const char *str, int base, bool safe) -{ - int errno_bak=errno; - errno=0; - char *endptr; -// _strtoi64 is available in Visual Studio, but not yet in MINGW -#ifdef _MSC_VER - const __int64 val=_strtoi64(str, &endptr, base); -#else - const long long val=strtoll(str, &endptr, base); -#endif - - if(safe) - { - CHECK_RETURN(0==errno); - errno=errno_bak; - CHECK_RETURN(endptr!=str); - if(std::numeric_limits::min()==0) - { - // unsigned - CHECK_RETURN(val>=0); - CHECK_RETURN( - (unsigned long long)(T)val<= - (unsigned long long)std::numeric_limits::max()); - } - else - { - // signed - CHECK_RETURN(val<=(long long)std::numeric_limits::max()); - CHECK_RETURN(val>=(long long)std::numeric_limits::min()); - } - } - - return (T)val; -} - unsigned safe_string2unsigned(const std::string &str, int base) { - return str2number(str.c_str(), base, true); + auto converted = string2optional(str, base); + CHECK_RETURN(converted != nullopt); + return *converted; } std::size_t safe_string2size_t(const std::string &str, int base) { - return str2number(str.c_str(), base, true); + auto converted = string2optional(str, base); + CHECK_RETURN(converted != nullopt); + return *converted; } int unsafe_string2int(const std::string &str, int base) { - return str2number(str.c_str(), base, false); + return narrow_cast(std::strtoll(str.c_str(), nullptr, base)); } unsigned unsafe_string2unsigned(const std::string &str, int base) { - return str2number(str.c_str(), base, false); + return narrow_cast(std::strtoul(str.c_str(), nullptr, base)); } std::size_t unsafe_string2size_t(const std::string &str, int base) { - return str2number(str.c_str(), base, false); + return narrow_cast(std::strtoull(str.c_str(), nullptr, base)); } signed long long int unsafe_string2signedlonglong( const std::string &str, int base) { - return str2number(str.c_str(), base, false); + return std::strtoll(str.c_str(), nullptr, false); } unsigned long long int unsafe_string2unsignedlonglong( const std::string &str, int base) { - return str2number(str.c_str(), base, false); + return *string2optional(str, base); +} + +optionalt string2optional_int(const std::string &str, int base) +{ + return string2optional(str, base); +} + +optionalt string2optional_unsigned(const std::string &str, int base) +{ + return string2optional(str, base); +} + +optionalt string2optional_size_t(const std::string &str, int base) +{ + return string2optional(str, base); } diff --git a/src/util/string2int.h b/src/util/string2int.h index 12c0260aaac..a6b87907c01 100644 --- a/src/util/string2int.h +++ b/src/util/string2int.h @@ -10,7 +10,10 @@ Author: Michael Tautschnig, michael.tautschnig@cs.ox.ac.uk #ifndef CPROVER_UTIL_STRING2INT_H #define CPROVER_UTIL_STRING2INT_H +#include "narrow.h" +#include "optional.h" #include +#include // These check that the string is indeed a valid number, // and fail an assertion otherwise. @@ -30,4 +33,85 @@ long long int unsafe_string2signedlonglong(const std::string &str, int base=10); long long unsigned int unsafe_string2unsignedlonglong( const std::string &str, int base=10); +// if we had a `resultt` รก la Boost.Outcome (https://ned14.github.io/outcome/) +// we could also return the reason why the conversion failed + +/// Convert string to integer as per stoi, but return nullopt when +/// stoi would throw +optionalt string2optional_int(const std::string &, int base = 10); + +/// Convert string to unsigned similar to the stoul or stoull functions, +/// return nullopt when the conversion fails. +/// Note: Unlike stoul or stoull negative inputs are disallowed +optionalt +string2optional_unsigned(const std::string &, int base = 10); + +/// Convert string to size_t similar to the stoul or stoull functions, +/// return nullopt when the conversion fails. +/// Note: Unlike stoul or stoull negative inputs are disallowed +optionalt +string2optional_size_t(const std::string &, int base = 10); + +/// convert string to signed long long if T is signed +template +auto string2optional_base(const std::string &str, int base) -> + typename std::enable_if::value, long long>::type +{ + static_assert( + sizeof(T) <= sizeof(long long), + "this works under the assumption that long long is the largest type we try " + "to convert"); + return std::stoll(str, nullptr, base); +} + +/// convert string to unsigned long long if T is unsigned +template +auto string2optional_base(const std::string &str, int base) -> + typename std::enable_if::value, unsigned long long>::type +{ + static_assert( + sizeof(T) <= sizeof(unsigned long long), + "this works under the assumption that long long is the largest type we try " + "to convert"); + if(str.find('-') != std::string::npos) + { + throw std::out_of_range{ + "unsigned conversion behaves a bit strangely with negative values, " + "therefore we disable it"}; + } + return std::stoull(str, nullptr, base); +} + +/// attempt a given conversion, return nullopt if the conversion fails +/// with out_of_range or invalid_argument +template +auto wrap_string_conversion(do_conversiont do_conversion) + -> optionalt +{ + try + { + return do_conversion(); + } + catch(const std::invalid_argument &) + { + return nullopt; + } + catch(const std::out_of_range &) + { + return nullopt; + } +} + +/// convert a string to an integer, given the base of the representation +/// works with signed and unsigned integer types smaller than +/// (unsigned) long long +/// does not accept negative inputs when the result type is unsigned +template +optionalt string2optional(const std::string &str, int base) +{ + return wrap_string_conversion([&]() { + return narrow_or_throw_out_of_range(string2optional_base(str, base)); + }); +} + #endif // CPROVER_UTIL_STRING2INT_H diff --git a/unit/Makefile b/unit/Makefile index 5a704019a5c..26de90c719b 100644 --- a/unit/Makefile +++ b/unit/Makefile @@ -69,7 +69,8 @@ SRC += analyses/ai/ai.cpp \ util/simplify_expr.cpp \ util/small_map.cpp \ util/small_shared_two_way_ptr.cpp \ - util/std_expr.cpp \ + util/std_expr.cpp \ + util/string2int.cpp \ util/string_utils/join_string.cpp \ util/string_utils/split_string.cpp \ util/string_utils/strip_string.cpp \ diff --git a/unit/util/string2int.cpp b/unit/util/string2int.cpp new file mode 100644 index 00000000000..a7c8d846bcb --- /dev/null +++ b/unit/util/string2int.cpp @@ -0,0 +1,88 @@ +/*******************************************************************\ + +Module: Unit tests for string2int.h + +Author: Diffblue Ltd. + +\*******************************************************************/ + +#include +#include + +TEST_CASE( + "converting optionally to a valid integer should succeed", + "[core][util][string2int]") +{ + REQUIRE(string2optional_int("13") == 13); + REQUIRE(string2optional_int("-5") == -5); + REQUIRE(string2optional_int("c0fefe", 16) == 0xc0fefe); +} + +TEST_CASE( + "optionally converting invalid string to integer should return nullopt", + "[core][util][string2int]") +{ + REQUIRE(string2optional_int("thirteen") == nullopt); + REQUIRE(string2optional_int("c0fefe") == nullopt); +} + +TEST_CASE( + "optionally converting string out of range to integer should return nullopt", + "[core][util][string2int]") +{ + REQUIRE( + string2optional_int("0xfffffffffffffffffffffffffffffffffffffffffff", 16) == + nullopt); +} + +TEST_CASE( + "converting optionally to a valid unsigned should succeed", + "[core][util][string2int]") +{ + REQUIRE(string2optional_unsigned("13") == 13u); + REQUIRE(string2optional_unsigned("c0fefe", 16) == 0xc0fefeu); +} + +TEST_CASE( + "optionally converting invalid string to unsigned should return nullopt", + "[core][util][string2int]") +{ + REQUIRE(string2optional_unsigned("thirteen") == nullopt); + REQUIRE(string2optional_unsigned("c0fefe") == nullopt); +} + +TEST_CASE( + "optionally converting string out of range to unsigned should return nullopt", + "[core][util][string2int]") +{ + REQUIRE( + string2optional_unsigned( + "0xfffffffffffffffffffffffffffffffffffffffffff", 16) == nullopt); + REQUIRE(string2optional_unsigned("-5") == nullopt); +} + +TEST_CASE( + "converting optionally to a valid size_t should succeed", + "[core][util][string2int]") +{ + REQUIRE(string2optional_size_t("13") == std::size_t{13}); + REQUIRE(string2optional_size_t("c0fefe", 16) == std::size_t{0xc0fefe}); +} + +TEST_CASE( + "optionally converting invalid string to size_t should return nullopt", + "[core][util][string2int]") +{ + REQUIRE(string2optional_size_t("thirteen") == nullopt); + REQUIRE(string2optional_size_t("c0fefe") == nullopt); +} + +TEST_CASE( + "optionally converting string out of range to size_t should return nullopt", + "[core][util][string2int]") +{ + REQUIRE( + string2optional_size_t( + "0xfffffffffffffffffffffffffffffffffffffffffff", 16) == nullopt); + REQUIRE(string2optional_size_t("-5") == nullopt); +}