diff --git a/simgear/misc/strutils.cxx b/simgear/misc/strutils.cxx index f20d5f68..6c9feb80 100644 --- a/simgear/misc/strutils.cxx +++ b/simgear/misc/strutils.cxx @@ -22,12 +22,13 @@ #include -#include -#include +#include #include #include -#include // strerror_r() and strerror_s() -#include +#include +#include // strerror_r() and strerror_s() +#include +#include #if defined(HAVE_CPP11_CODECVT) #include // new in C++11 @@ -374,6 +375,173 @@ namespace simgear { return result; } + template<> + int digitValue<10>(char c) + { + if ('0' <= c && c <= '9') { + return static_cast(c - '0'); + } else { + throw sg_range_exception("invalid as a decimal digit: '" + + std::string(1, c) + "'"); + } + } + + template<> + int digitValue<16>(char c) + { + if ('0' <= c && c <= '9') { + return static_cast(c - '0'); + } else if ('a' <= c && c <= 'f') { + return 10 + static_cast(c - 'a'); + } else if ('A' <= c && c <= 'F') { + return 10 + static_cast(c - 'A'); + } else { + throw sg_range_exception("invalid as an hexadecimal digit: '" + + std::string(1, c) + "'"); + } + } + + template<> + std::string numerationBaseAdjective<10>() + { return std::string("decimal"); } + + template<> + std::string numerationBaseAdjective<16>() + { return std::string("hexadecimal"); } + + template< + class T, + int BASE = 10, + typename = typename std::enable_if::value, T>::type > + T readNonNegativeInt(const std::string& s) + { + static_assert(0 < BASE, + "template value BASE must be a positive integer"); + static_assert(BASE <= std::numeric_limits::max(), + "template type T too small: it cannot represent BASE"); + T res(0); + T multiplier(1); + T increment; + int digit; + + if (s.empty()) { + throw sg_format_exception("expected a non-empty string", s); + } + + for (auto it = s.crbegin(); it != s.crend(); it++) { + if (it != s.crbegin()) { + // Check if 'multiplier *= BASE' is going to overflow. This is + // reliable because 'multiplier' and 'BASE' are positive. + if (multiplier > std::numeric_limits::max() / BASE) { + // If all remaining digits are '0', it doesn't matter that + // the multiplier overflows. + if (std::all_of(it, s.crend(), + [](char c){ return (c == '0'); })) { + return res; + } else { + throw sg_range_exception( + "doesn't fit in the specified type: '" + s + "'"); + } + } + multiplier *= BASE; + } + + try { + digit = digitValue(*it); + } catch (const sg_range_exception&) { + throw sg_format_exception( + "expected a string containing " + + numerationBaseAdjective() + + " digits only, but got '" + s + "'", s); + } + + // Reliable because 'multiplier' is positive + if (digit > 0 && + multiplier > std::numeric_limits::max() / digit) { + throw sg_range_exception( + "doesn't fit in the specified type: '" + s + "'"); + } + increment = multiplier*digit; + + if (res > std::numeric_limits::max() - increment) { + throw sg_range_exception( + "doesn't fit in the specified type: '" + s + "'"); + } + res += increment; + } + + return res; + } + + // Explicit template instantiations. + // + // In order to save some bytes for the SimGearCore library[*], we only + // instantiate a small number of variants of readNonNegativeInt() below. + // Just enable the ones you need if they are disabled. + // + // [*] The exact amount depends a lot on what you measure and in which + // circumstances. On Linux amd64 with g++, I measured a cost ranging + // from 2 KB per template in a Release build to 19 KB per template in + // a RelWithDebInfo build for the in-memory code size of the resulting + // fgfs binary (CODE column in 'top', after selecting a suitable + // unit). If I look at the fgfs binary size (statically-linked with + // SimGear), I measure from 2 KB per template (Release) to 30 KB per + // template (RelWithDebInfo). Finally, a Debug build compiled with + // '-fno-omit-frame-pointer -O0 -fno-inline' lies between the Release + // and the RelWithDebInfo builds. + +#if 0 + template + signed char readNonNegativeInt(const std::string& s); + template + signed char readNonNegativeInt(const std::string& s); + template + unsigned char readNonNegativeInt(const std::string& s); + template + unsigned char readNonNegativeInt(const std::string& s); + + template + short readNonNegativeInt(const std::string& s); + template + short readNonNegativeInt(const std::string& s); + template + unsigned short readNonNegativeInt(const std::string& s); + template + unsigned short readNonNegativeInt(const std::string& s); +#endif + + template + int readNonNegativeInt(const std::string& s); + template + unsigned int readNonNegativeInt(const std::string& s); + +#if 0 + template + int readNonNegativeInt(const std::string& s); + template + unsigned int readNonNegativeInt(const std::string& s); + + template + long readNonNegativeInt(const std::string& s); + template + long readNonNegativeInt(const std::string& s); + template + unsigned long readNonNegativeInt(const std::string& s); + template + unsigned long readNonNegativeInt(const std::string& s); + + template + long long readNonNegativeInt(const std::string& s); + template + long long readNonNegativeInt(const std::string& s); + template + unsigned long long readNonNegativeInt( + const std::string& s); + template + unsigned long long readNonNegativeInt( + const std::string& s); +#endif + int compare_versions(const string& v1, const string& v2, int maxComponents) { vector v1parts(split(v1, ".")); diff --git a/simgear/misc/strutils.hxx b/simgear/misc/strutils.hxx index 73c1d387..ed0ff014 100644 --- a/simgear/misc/strutils.hxx +++ b/simgear/misc/strutils.hxx @@ -31,6 +31,7 @@ #include #include +#include #include typedef std::vector < std::string > string_list; @@ -169,6 +170,44 @@ namespace simgear { */ int to_int(const std::string& s, int base = 10); + /** Convert a char to the integer it represents in the specified BASE. + * + * Contrary to std::isdigit() and std::isxdigit(), only the standard ASCII + * digits for BASE are accepted (with both uppercase and lowercase 'a'-'f' + * letters for base 16). Throw sg_range_exception if the char is not a + * valid digit for this base. + * + * See template specializations in strutils.cxx. + */ + template + int digitValue(char c); + + /** Return: + * - std::string("decimal") if BASE is 10; + * - std::string("hexadecimal") if BASE is 16. + * + * Template specializations in strutils.cxx. + */ + template + std::string numerationBaseAdjective(); + + /** Convert a string representing an integer to an integral type. + * + * The input string must be non-empty and contain only digits of the + * specified BASE (template parameter). Throw: + * - sg_format_exception if the input string doesn't respect these + * constraints; + * - sg_range_exception if the value can't be represented by type T + * (i.e., if it is too large). + * + * Explicit template instantiations are added as needed in strutils.cxx. + * Have a look there and enable the ones you need! + */ + template< + class T, + int BASE = 10, + typename = typename std::enable_if::value, T>::type > + T readNonNegativeInt(const std::string& s); /** * Convert a string representing a boolean, to a bool. diff --git a/simgear/misc/strutils_test.cxx b/simgear/misc/strutils_test.cxx index 7f89509b..dd686fa2 100644 --- a/simgear/misc/strutils_test.cxx +++ b/simgear/misc/strutils_test.cxx @@ -6,12 +6,19 @@ #include #include // std::move() #include // std::ifstream -#include +#include // std::ostringstream +#include // std::dec, std::hex +#include // std::numeric_limits +#include // typeid() +#include // uint16_t, uintmax_t, etc. #include // _set_errno() on Windows +#include +#include #include #include #include +#include using std::string; using std::vector; @@ -90,6 +97,290 @@ void test_to_int() SG_CHECK_EQUAL(strutils::to_int("-10000"), -10000); } +// Auxiliary function for test_readNonNegativeInt() +void aux_readNonNegativeInt_setUpOStringStream(std::ostringstream& oss, int base) +{ + switch (base) { + case 10: + oss << std::dec; + break; + case 16: + oss << std::hex; + break; + default: + SG_TEST_FAIL("unsupported value for 'base': " + std::to_string(base)); + } +} + +// Auxiliary function for test_readNonNegativeInt(): round-trip conversion for +// the given number of values below and up to std::numeric_limits::max(). +template +void aux_readNonNegativeInt_testValuesCloseToMax(T nbValues) +{ + std::ostringstream oss; + + assert(0 <= nbValues && nbValues <= std::numeric_limits::max()); + aux_readNonNegativeInt_setUpOStringStream(oss, BASE); + + for (T i = std::numeric_limits::max() - nbValues; + i < std::numeric_limits::max(); i++) { + T valueToTest = i + 1; + T roundTripResult; + bool gotException = false; + + oss.str(""); + // The cast is only useful when T is a char type + oss << static_cast(valueToTest); + + try { + roundTripResult = strutils::readNonNegativeInt(oss.str()); + } catch (const sg_range_exception&) { + gotException = true; + } + + SG_VERIFY(!gotException); + SG_CHECK_EQUAL(roundTripResult, valueToTest); + } +} + +// Auxiliary class for test_readNonNegativeInt(): test that we do get an +// exception when trying to convert the smallest, positive out-of-range value +// for type T. +template +class ReadNonNegativeInt_JustOutOfRangeTester { +public: + ReadNonNegativeInt_JustOutOfRangeTester() + { } + + // Run the test + void run() + { + std::ostringstream oss; + aux_readNonNegativeInt_setUpOStringStream(oss, BASE); + oss << 1 + static_cast(std::numeric_limits::max()); + bool gotException = false; + + try { + strutils::readNonNegativeInt(oss.str()); + } catch (const sg_range_exception&) { + gotException = true; + } + + SG_VERIFY(gotException); + } +}; + +class ReadNonNegativeInt_DummyTester { +public: + ReadNonNegativeInt_DummyTester() + { } + + void run() + { } +}; + +// We use this helper class to automatically determine for which types +// ReadNonNegativeInt_JustOutOfRangeTester::run() can be run. +template +class AuxReadNonNegativeInt_JustOutOfRange_Helper +{ + typedef typename std::make_unsigned::type uT; + + // Define TestRunner to be either + // + // ReadNonNegativeInt_JustOutOfRangeTester + // + // or + // + // ReadNonNegativeInt_DummyTester + // + // depending on whether 1 + std::numeric_limits::max() can be + // represented by uintmax_t. + typedef typename std::conditional< + static_cast(std::numeric_limits::max()) < + std::numeric_limits::max(), + ReadNonNegativeInt_JustOutOfRangeTester, + ReadNonNegativeInt_DummyTester >::type TestRunner; + +public: + AuxReadNonNegativeInt_JustOutOfRange_Helper() + { }; + + void test() + { + TestRunner().run(); + } +}; + +void test_readNonNegativeInt() +{ +// In order to save some bytes for the SimGearCore library[*], we only +// instantiated a small number of variants of readNonNegativeInt() in +// strutils.cxx. This is why many tests are disabled with '#if 0' below. Of +// course, more variants can be enabled when they are needed. +// +// [*] See measures in strutils.cxx before the template instantiations. + +#if 0 + SG_CHECK_EQUAL((strutils::readNonNegativeInt("0")), 0); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("23")), 23); +#endif + + SG_CHECK_EQUAL((strutils::readNonNegativeInt("0")), 0); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("00000000")), 0); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("12345")), 12345); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("12345")), 12345); + +#if 0 + SG_CHECK_EQUAL((strutils::readNonNegativeInt("ff")), 0xff); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("a5E9")), 0xa5e9); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("0cda")), + 0x0cda); + + SG_CHECK_EQUAL((strutils::readNonNegativeInt("65535")), 0xffff); + SG_CHECK_EQUAL( + (strutils::readNonNegativeInt("00000000000000000000065535")), + 0xffff); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("ffff")), 0xffff); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("32767")), 0x7fff); + SG_CHECK_EQUAL((strutils::readNonNegativeInt("7fff")), 0x7fff); +#endif + + // Nothing special about the values :) + SG_CHECK_EQUAL_NOSTREAM((typeid(strutils::readNonNegativeInt("72"))), + typeid(int(12))); +#if 0 + SG_CHECK_EQUAL_NOSTREAM((typeid(strutils::readNonNegativeInt("72"))), + typeid(12L)); + SG_CHECK_EQUAL_NOSTREAM( + (typeid(strutils::readNonNegativeInt("72"))), + typeid(12LL)); +#endif + + { + bool gotException = false; + try { + strutils::readNonNegativeInt(""); // empty string: illegal + } catch (const sg_format_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + } + + { + bool gotException = false; + try { + strutils::readNonNegativeInt("-1"); // non-digit character: illegal + } catch (const sg_format_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + } + + { + bool gotException = false; + try { + strutils::readNonNegativeInt("+1"); // non-digit character: illegal + } catch (const sg_format_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + } + + { + bool gotException = false; + try { + strutils::readNonNegativeInt("858efe"); // trailing garbage: illegal + } catch (const sg_format_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + } + +#if 0 + { + bool gotException = false; + try { + strutils::readNonNegativeInt("858g5k"); // ditto for base 16 + } catch (const sg_format_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + } +#endif + + { + bool gotException = false; + try { + strutils::readNonNegativeInt(" 858"); // leading whitespace/garbage: + } catch (const sg_format_exception&) { // illegal too + gotException = true; + } + SG_VERIFY(gotException); + } + + // Try to read a value that is 1 unit too large for the type. Check that it + // raises an sg_range_exception in each case. +#if 0 + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); +#endif + + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + +#if 0 + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); + AuxReadNonNegativeInt_JustOutOfRange_Helper().test(); +#endif + + // Round trip tests with large values, including the largest value that can + // be represented by the type, in each case. + // + // Can be casted as any of the following types + constexpr int nbValues = 5000; +#if 0 + aux_readNonNegativeInt_testValuesCloseToMax(127); + aux_readNonNegativeInt_testValuesCloseToMax(127); + aux_readNonNegativeInt_testValuesCloseToMax(127); + aux_readNonNegativeInt_testValuesCloseToMax(127); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); +#endif + + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + +#if 0 + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); + aux_readNonNegativeInt_testValuesCloseToMax(nbValues); +#endif +} + void test_split() { string_list l = strutils::split("zero one two three four five"); @@ -299,6 +590,7 @@ int main(int argc, char* argv[]) test_ends_with(); test_simplify(); test_to_int(); + test_readNonNegativeInt(); test_split(); test_escape(); test_unescape();