Avoid possibility of overflow in DateTime parsing functions

* This is strictly undefined behavior so let's avoid it
* As a side-effect it is now possible to omit the separators in
  DateTime::fromIsoString()
This commit is contained in:
Martchus 2021-05-24 21:27:18 +02:00
parent 2e93882882
commit a116c9e790
2 changed files with 33 additions and 13 deletions

View File

@ -91,8 +91,7 @@ DateTime DateTime::fromString(const char *str)
miliSeconds += (c - '0') * miliSecondsFact; miliSeconds += (c - '0') * miliSecondsFact;
miliSecondsFact /= 10; miliSecondsFact /= 10;
} else { } else {
*valueIndex *= 10; Detail::raiseAndAdd(*valueIndex, 10, c);
*valueIndex += c - '0';
} }
} else if ((c == '-' || c == ':' || c == '/') || (c == '.' && (valueIndex == secondsIndex)) || (c == ' ' && (valueIndex == dayIndex))) { } else if ((c == '-' || c == ':' || c == '/') || (c == '.' && (valueIndex == secondsIndex)) || (c == ' ' && (valueIndex == dayIndex))) {
if (++valueIndex == valuesEnd) { if (++valueIndex == valuesEnd) {
@ -101,7 +100,7 @@ DateTime DateTime::fromString(const char *str)
} else if (c == '\0') { } else if (c == '\0') {
break; break;
} else { } else {
throw ConversionException(argsToString("unexpected ", c)); throw ConversionException(argsToString("Unexpected character \"", c, '\"'));
} }
} }
return DateTime::fromDateAndTime(values[0], values[1], *dayIndex, values[3], values[4], *secondsIndex, miliSeconds); return DateTime::fromDateAndTime(values[0], values[1], *dayIndex, values[3], values[4], *secondsIndex, miliSeconds);
@ -110,10 +109,11 @@ DateTime DateTime::fromString(const char *str)
/*! /*!
* \brief Parses the specified ISO date time denotation provided as C-style string. * \brief Parses the specified ISO date time denotation provided as C-style string.
* \returns Returns a pair where the first value is the parsed date time and the second value * \returns Returns a pair where the first value is the parsed date time and the second value
* a time span which can be subtracted from the first value to get the UTC time. * the time zone designator (a time span which can be subtracted from the first value to get the UTC time).
* \remarks Not all variants allowed by ISO 8601 are supported right now, eg. delimiters can not * \remarks
* be omitted. * - Parsing durations and time intervals is *not* supported.
* The common form (something like "2016-08-29T21:32:31.588539814+02:00") is supported of course. * - Truncated representations are *not* supported.
* - Standardised extensions (ISO 8601-2:2019) are *not* supported.
* \sa https://en.wikipedia.org/wiki/ISO_8601 * \sa https://en.wikipedia.org/wiki/ISO_8601
*/ */
std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str) std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
@ -126,7 +126,9 @@ std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
int *const secondsIndex = values + 5; int *const secondsIndex = values + 5;
int *const miliSecondsIndex = values + 6; int *const miliSecondsIndex = values + 6;
int *const deltaHourIndex = values + 7; int *const deltaHourIndex = values + 7;
int *const valuesEnd = values + 9;
int *valueIndex = values; int *valueIndex = values;
unsigned int remainingDigits = 4;
bool deltaNegative = false; bool deltaNegative = false;
double miliSecondsFact = 100.0, miliSeconds = 0.0; double miliSecondsFact = 100.0, miliSeconds = 0.0;
for (const char *strIndex = str;; ++strIndex) { for (const char *strIndex = str;; ++strIndex) {
@ -136,13 +138,21 @@ std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
miliSeconds += (c - '0') * miliSecondsFact; miliSeconds += (c - '0') * miliSecondsFact;
miliSecondsFact /= 10; miliSecondsFact /= 10;
} else { } else {
if (!remainingDigits) {
if (++valueIndex == miliSecondsIndex || valueIndex >= valuesEnd) {
throw ConversionException("Max. number of digits exceeded");
}
remainingDigits = 2;
}
*valueIndex *= 10; *valueIndex *= 10;
*valueIndex += c - '0'; *valueIndex += c - '0';
remainingDigits -= 1;
} }
} else if (c == 'T') { } else if (c == 'T') {
if (++valueIndex != hourIndex) { if (++valueIndex != hourIndex) {
throw ConversionException("\"T\" expected before hour"); throw ConversionException("\"T\" expected before hour");
} }
remainingDigits = 2;
} else if (c == '-') { } else if (c == '-') {
if (valueIndex < dayIndex) { if (valueIndex < dayIndex) {
++valueIndex; ++valueIndex;
@ -150,34 +160,38 @@ std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
valueIndex = deltaHourIndex; valueIndex = deltaHourIndex;
deltaNegative = true; deltaNegative = true;
} else { } else {
throw ConversionException("unexpected \"-\" after day"); throw ConversionException("Unexpected \"-\" after day");
} }
remainingDigits = 2;
} else if (c == '.') { } else if (c == '.') {
if (valueIndex != secondsIndex) { if (valueIndex != secondsIndex) {
throw ConversionException("unexpected \".\""); throw ConversionException("Unexpected \".\"");
} else { } else {
++valueIndex; ++valueIndex;
} }
} else if (c == ':') { } else if (c == ':') {
if (valueIndex < hourIndex) { if (valueIndex < hourIndex) {
throw ConversionException("unexpected \":\" before hour"); throw ConversionException("Unexpected \":\" before hour");
} else if (valueIndex == secondsIndex) { } else if (valueIndex == secondsIndex) {
throw ConversionException("unexpected \":\" after second"); throw ConversionException("Unexpected \":\" after second");
} else { } else {
++valueIndex; ++valueIndex;
} }
remainingDigits = 2;
} else if ((c == '+') && (++valueIndex >= secondsIndex)) { } else if ((c == '+') && (++valueIndex >= secondsIndex)) {
valueIndex = deltaHourIndex; valueIndex = deltaHourIndex;
deltaNegative = false; deltaNegative = false;
remainingDigits = 2;
} else if ((c == 'Z') && (++valueIndex >= secondsIndex)) { } else if ((c == 'Z') && (++valueIndex >= secondsIndex)) {
valueIndex = deltaHourIndex + 2; valueIndex = deltaHourIndex + 2;
remainingDigits = 2;
} else if (c == '\0') { } else if (c == '\0') {
break; break;
} else { } else {
throw ConversionException(argsToString("unexpected \"", c, '\"')); throw ConversionException(argsToString("Unexpected \"", c, '\"'));
} }
} }
auto delta(TimeSpan::fromMinutes(*deltaHourIndex * 60 + values[8])); auto delta = TimeSpan::fromMinutes(*deltaHourIndex * 60 + values[8]);
if (deltaNegative) { if (deltaNegative) {
delta = TimeSpan(-delta.totalTicks()); delta = TimeSpan(-delta.totalTicks());
} }

View File

@ -182,6 +182,12 @@ void ChronoTests::testDateTime()
const auto test7 = DateTime::fromIsoString("2021-05-20T23:02:45-04:00"); const auto test7 = DateTime::fromIsoString("2021-05-20T23:02:45-04:00");
CPPUNIT_ASSERT_EQUAL_MESSAGE("no seconds fraction (negative timezone offset, 1)", DateTime::fromDateAndTime(2021, 5, 20, 23, 2, 45), test7.first); CPPUNIT_ASSERT_EQUAL_MESSAGE("no seconds fraction (negative timezone offset, 1)", DateTime::fromDateAndTime(2021, 5, 20, 23, 2, 45), test7.first);
CPPUNIT_ASSERT_EQUAL_MESSAGE("no seconds fraction (negative timezone offset, 2)", TimeSpan::fromHours(-4.0), test7.second); CPPUNIT_ASSERT_EQUAL_MESSAGE("no seconds fraction (negative timezone offset, 2)", TimeSpan::fromHours(-4.0), test7.second);
// implied separators / too many digits
CPPUNIT_ASSERT_EQUAL_MESSAGE("no separators", test5.first - test5.second, DateTime::fromIsoStringGmt("20170823T194015.985077682-0230"));
CPPUNIT_ASSERT_EQUAL_MESSAGE(
"not even T separator", DateTime::fromDateAndTime(2017, 8, 23, 19, 40, 15), DateTime::fromIsoStringGmt("20170823194015"));
CPPUNIT_ASSERT_THROW_MESSAGE("too many digits after seconds", DateTime::fromIsoString("2017082319401516"), ConversionException);
CPPUNIT_ASSERT_THROW_MESSAGE("too many digits after timezone offset", DateTime::fromIsoString("20170823194015.16+02300"), ConversionException);
// test invalid characters // test invalid characters
CPPUNIT_ASSERT_THROW_MESSAGE("digits after Z", DateTime::fromIsoString("2017-O8-23T19:40:15.985077682Z02:00"), ConversionException); CPPUNIT_ASSERT_THROW_MESSAGE("digits after Z", DateTime::fromIsoString("2017-O8-23T19:40:15.985077682Z02:00"), ConversionException);
CPPUNIT_ASSERT_THROW_MESSAGE("invalid letter", DateTime::fromIsoString("2017-O8-23T19:40:15.985077682:+02:00"), ConversionException); CPPUNIT_ASSERT_THROW_MESSAGE("invalid letter", DateTime::fromIsoString("2017-O8-23T19:40:15.985077682:+02:00"), ConversionException);