Skip to content

Commit

Permalink
enh(Foundation): DateTimeParser: stricter checks of timezones, more t…
Browse files Browse the repository at this point in the history
…ests for invalid inputs. (#569)
  • Loading branch information
matejk committed Dec 9, 2023
1 parent fc50950 commit d900af8
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 51 deletions.
4 changes: 3 additions & 1 deletion Foundation/src/DateTimeFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const std::string DateTimeFormat::ISO8601_REGEX("([\\+-]?\\d{4}(?!\\d{2}\\b))"

const std::string DateTimeFormat::RFC822_FORMAT("%w, %e %b %y %H:%M:%S %Z");

// TODO: Is this regex correct? RFC 822 spec does not mention timezone codes, just GMT.
const std::string DateTimeFormat::RFC822_REGEX("(((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)), *)?"
"\\d\\d? +"
"((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec)) +"
Expand Down Expand Up @@ -75,6 +74,9 @@ const std::string DateTimeFormat::RFC1036_REGEX(
"(" TIMEZONES_REGEX_PART "|)?+"
"(([+\\-]?\\d\\d\\d\\d)?|" TIMEZONES_REGEX_PART "|\\w)");

// It would perhaps be useful to add RFC 2822 (successor of 822)
// https://www.rfc-editor.org/rfc/rfc2822#section-3.3

const std::string DateTimeFormat::ASCTIME_FORMAT("%w %b %f %H:%M:%S %Y");
const std::string DateTimeFormat::ASCTIME_REGEX("((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)) +"
"((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec)) +"
Expand Down
102 changes: 62 additions & 40 deletions Foundation/src/DateTimeParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& str, DateT
int micros = 0;
int tzd = 0;

bool dayParsed = false;
bool monthParsed = false;

std::string::const_iterator it = str.begin();
std::string::const_iterator end = str.end();
std::string::const_iterator itf = fmt.begin();
Expand All @@ -78,18 +81,21 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& str, DateT
case 'b':
case 'B':
month = parseMonth(it, end);
monthParsed = true;
break;
case 'd':
case 'e':
case 'f':
SKIP_JUNK();
PARSE_NUMBER_N(day, 2);
dayParsed = true;
break;
case 'm':
case 'n':
case 'o':
SKIP_JUNK();
PARSE_NUMBER_N(month, 2);
monthParsed = true;
break;
case 'y':
SKIP_JUNK();
Expand Down Expand Up @@ -167,12 +173,13 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& str, DateT
}
else ++itf;
}
if (month == 0) month = 1;
if (day == 0) day = 1;
if (!monthParsed) month = 1;
if (!dayParsed) day = 1;
if (DateTime::isValid(year, month, day, hour, minute, second, millis, micros))
dateTime.assign(year, month, day, hour, minute, second, millis, micros);
else
throw SyntaxException("date/time component out of range");

timeZoneDifferential = tzd;
}

Expand All @@ -191,7 +198,7 @@ bool DateTimeParser::tryParse(const std::string& fmt, const std::string& str, Da
{
parse(fmt, str, dateTime, timeZoneDifferential);
}
catch (Exception&)
catch (const Exception&)
{
return false;
}
Expand Down Expand Up @@ -245,53 +252,55 @@ int DateTimeParser::parseTZD(std::string::const_iterator& it, const std::string:
{
const char* designator;
int timeZoneDifferential;
bool allowsDifference;
};

static Zone zones[] =
static const Zone zones[] =
{
{"Z", 0},
{"UT", 0},
{"GMT", 0},
{"BST", 1*3600},
{"IST", 1*3600},
{"WET", 0},
{"WEST", 1*3600},
{"CET", 1*3600},
{"CEST", 2*3600},
{"EET", 2*3600},
{"EEST", 3*3600},
{"MSK", 3*3600},
{"MSD", 4*3600},
{"NST", -3*3600-1800},
{"NDT", -2*3600-1800},
{"AST", -4*3600},
{"ADT", -3*3600},
{"EST", -5*3600},
{"EDT", -4*3600},
{"CST", -6*3600},
{"CDT", -5*3600},
{"MST", -7*3600},
{"MDT", -6*3600},
{"PST", -8*3600},
{"PDT", -7*3600},
{"AKST", -9*3600},
{"AKDT", -8*3600},
{"HST", -10*3600},
{"AEST", 10*3600},
{"AEDT", 11*3600},
{"ACST", 9*3600+1800},
{"ACDT", 10*3600+1800},
{"AWST", 8*3600},
{"AWDT", 9*3600}
{"Z", 0, true},
{"UT", 0, true},
{"GMT", 0, true},
{"BST", 1*3600, false},
{"IST", 1*3600, false},
{"WET", 0, false},
{"WEST", 1*3600, false},
{"CET", 1*3600, false},
{"CEST", 2*3600, false},
{"EET", 2*3600, false},
{"EEST", 3*3600, false},
{"MSK", 3*3600, false},
{"MSD", 4*3600, false},
{"NST", -3*3600-1800, false},
{"NDT", -2*3600-1800, false},
{"AST", -4*3600, false},
{"ADT", -3*3600, false},
{"EST", -5*3600, false},
{"EDT", -4*3600, false},
{"CST", -6*3600, false},
{"CDT", -5*3600, false},
{"MST", -7*3600, false},
{"MDT", -6*3600, false},
{"PST", -8*3600, false},
{"PDT", -7*3600, false},
{"AKST", -9*3600, false},
{"AKDT", -8*3600, false},
{"HST", -10*3600, false},
{"AEST", 10*3600, false},
{"AEDT", 11*3600, false},
{"ACST", 9*3600+1800, false},
{"ACDT", 10*3600+1800, false},
{"AWST", 8*3600, false},
{"AWDT", 9*3600, false}
};

int tzd = 0;
while (it != end && Ascii::isSpace(*it)) ++it;
const Zone* zone = nullptr;
std::string designator;
if (it != end)
{
if (Ascii::isAlpha(*it))
{
std::string designator;
designator += *it++;
if (it != end && Ascii::isAlpha(*it)) designator += *it++;
if (it != end && Ascii::isAlpha(*it)) designator += *it++;
Expand All @@ -300,20 +309,33 @@ int DateTimeParser::parseTZD(std::string::const_iterator& it, const std::string:
{
if (designator == zones[i].designator)
{
tzd = zones[i].timeZoneDifferential;
zone = &(zones[i]);
tzd = zone->timeZoneDifferential;
break;
}
}
}
if (!designator.empty() && !zone)
throw SyntaxException("Unknown timezone designator "s + designator);

if (it != end && (*it == '+' || *it == '-'))
{
// Time difference is allowed only for some timezone designators in general
// Some formats prevent even that with regular expression
if (zone && !zone->allowsDifference)
throw SyntaxException("Timezone does not allow difference "s + zone->designator);

int sign = *it == '+' ? 1 : -1;
++it;
int hours = 0;
PARSE_NUMBER_N(hours, 2);
if (hours < 0 || hours > 23)
throw SyntaxException("Timezone difference hours out of range");
if (it != end && *it == ':') ++it;
int minutes = 0;
PARSE_NUMBER_N(minutes, 2);
if (minutes < 0 || minutes > 59)
throw SyntaxException("Timezone difference minutes out of range");
tzd += sign*(hours*3600 + minutes*60);
}
}
Expand Down
26 changes: 16 additions & 10 deletions Foundation/testsuite/src/DateTimeParserTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ using Poco::DateTimeParser;
using Poco::Timestamp;
using Poco::SyntaxException;

using namespace std::string_literals;

DateTimeParserTest::DateTimeParserTest(const std::string& name): CppUnit::TestCase(name)
{
Expand All @@ -47,6 +48,16 @@ void DateTimeParserTest::testISO8601()
assertTrue (dt.second() == 0);
assertTrue (tzd == 0);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-08T12.30:00Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-00-08T12:30:00Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-00T12:30:00Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-00T33:30:00Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-00T12:80:00Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-00T12:30:90Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-0012:30:90Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-00X12:30:90Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "200501-00T12:30:90Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-0100T12:30:90Z", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005_01+00T12:30:90Z", tzd);

dt = DateTimeParser::parse(DateTimeFormat::ISO8601_FORMAT, "2005-01-08T12:30:00+01:00", tzd);
assertTrue (dt.year() == 2005);
Expand All @@ -57,6 +68,8 @@ void DateTimeParserTest::testISO8601()
assertTrue (dt.second() == 0);
assertTrue (tzd == 3600);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-8T12:30:00+01:00", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-08T12:30:00+41:00", tzd);
testBad(DateTimeFormat::ISO8601_FRAC_FORMAT, "2005-01-08T12:30:00+01:70", tzd);

dt = DateTimeParser::parse(DateTimeFormat::ISO8601_FORMAT, "2005-01-08T12:30:00-01:00", tzd);
assertTrue (dt.year() == 2005);
Expand Down Expand Up @@ -351,15 +364,7 @@ void DateTimeParserTest::testRFC1123()
assertTrue (tzd == -14400);
testBad(DateTimeFormat::RFC1123_FORMAT, "Hue, 18 Jan 2005 12:30:00 EDT", tzd);

// Parsing fails? If this format supported at all by the spec?
dt = DateTimeParser::parse(DateTimeFormat::RFC1123_FORMAT, "Sun, 20 Jul 1969 16:17:30 GMT+01:00", tzd);
assertTrue (dt.year() == 1969);
assertTrue (dt.month() == 7);
assertTrue (dt.day() == 20);
assertTrue (dt.hour() == 16);
assertTrue (dt.minute() == 17);
assertTrue (dt.second() == 30);
assertTrue (tzd == 3600);
testBad(DateTimeFormat::RFC1123_FORMAT, "Sun, 20 Jul 1969 16:17:30 GMT+01:00", tzd);
testBad(DateTimeFormat::RFC1123_FORMAT, "Sun, 20 Jul 1969 16:17:30 GMT+01?00", tzd);

dt = DateTimeParser::parse(DateTimeFormat::RFC1123_FORMAT, "Tue, 18 Jan 2005 12:30:00 CDT", tzd);
Expand Down Expand Up @@ -391,6 +396,7 @@ void DateTimeParserTest::testRFC1123()
assertTrue (dt.second() == 12);
assertTrue (tzd == -18000);
testBad(DateTimeFormat::RFC1123_FORMAT, "12 Sep 193 02:01:12 EST", tzd);
testBad(DateTimeFormat::RFC1123_FORMAT, "12 Sep 1973 02:01:12 ABC", tzd);
}


Expand Down Expand Up @@ -840,7 +846,7 @@ void DateTimeParserTest::testBad(const std::string& fmt, const std::string& date
{
DateTime dt;
DateTimeParser::parse(fmt, dateStr, dt, tzd);
fail ("must fail");
fail ("must fail: "s + fmt + ", " + dateStr);
}
catch(const Poco::Exception) { }

Check warning

Code scanning / CodeQL

Catching by value Warning

This should catch a Exception by (const) reference rather than by value.
}
Expand Down

0 comments on commit d900af8

Please sign in to comment.