Skip to content

Commit b8d2b50

Browse files
kozemcakakozemcakS
andauthored
Fix: DateTimeParser trailing timezone designators & Timezone UTC offset cache invalidation (#5318)
* fix(Foundation): Timezone: invalidate utcOffset cache when /etc/localtime changes Poco commit 1850dc1 introduced a TZInfo cache for the UTC offset to avoid repeated tzset() syscalls. The cache is invalidated only when the TZ environment variable changes. However, the TZ variable is process-local: if a different process (e.g. a timezone configuration daemon or an init script) changes the system timezone by updating /etc/localtime, the running process is not notified and its TZ environment variable remains unchanged. On systems that switch timezone by updating /etc/localtime (a symlink) without touching the TZ env var, the cache is therefore never invalidated and Timezone::utcOffset() returns the stale value computed at startup. Fix by extending cacheTZ()/tzChanged() to also track the inode and mtime of /etc/localtime via stat(2). When either changes the cache is considered stale and reloaded, preserving the performance benefit for the common case where neither TZ nor /etc/localtime changes between calls. Signed-off-by: Andrej Kozemcak <andrej.kozemcak@siemens.com> * fix(Foundation): DateTimeParser: %S consume optional fractional seconds Parsing ISO 8601 date strings that contain both fractional seconds and a timezone offset (e.g. "2013-10-07T08:23:19.120-04:00") with the format "%Y-%m-%dT%H:%M:%S%z" raises a SyntaxException: - %S consumes the integer seconds but stops at '.', leaving ".120-04:00" unconsumed. - %z (parseTZD) is called next but sees '.' and returns without consuming anything. - The trailing-garbage check then raises SyntaxException. Extend the %S case to consume and discard an optional fractional-second suffix ('.' or ',' followed by one or more digits) immediately after parsing the integer seconds. This mirrors the existing %s behaviour and allows %z to see the timezone designator directly, keeping the trailing-garbage check fully effective for truly invalid input. Fix suggested by matejk. Signed-off-by: Andrej Kozemcak <andrej.kozemcak@siemens.com> * test(DateTimeParserTest): add ISO8601 fractional seconds parser test Add testISO8601FracSeconds to verify that DateTimeParser correctly handles fractional-second suffixes (dot and comma separated) in ISO8601_FORMAT strings, and rejects malformed input such as a bare decimal point with no digits. --------- Signed-off-by: Andrej Kozemcak <andrej.kozemcak@siemens.com> Co-authored-by: Andrej Kozemcak <andrej.kozemcak@siemens.com>
1 parent 11450e0 commit b8d2b50

5 files changed

Lines changed: 86 additions & 1 deletion

File tree

Foundation/include/Poco/DateTimeParser.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class Foundation_API DateTimeParser
6363
/// Throws a SyntaxException if the string cannot be successfully parsed.
6464
/// Please see DateTimeFormatter::format() for a description of the format string.
6565
/// Class DateTimeFormat defines format strings for various standard date/time formats.
66+
///
67+
/// Note: The %S specifier parses whole seconds and then silently discards any
68+
/// fractional-second suffix of the form '.DDD' or ',DDD' (including a bare decimal
69+
/// point that is immediately followed by a non-digit is treated as a parse error).
70+
/// Callers that need the fractional digits captured must use %s (millis+micros),
71+
/// %i (milliseconds only), %c (centiseconds), or %F (six-digit fractional seconds).
6672

6773
static DateTime parse(const std::string& fmt, const std::string& str, int& timeZoneDifferential);
6874
/// Parses a date and time in the given format from the given string.

Foundation/src/DateTimeParser.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ void DateTimeParser::parse(const std::string& fmt, const std::string& dtStr, Dat
243243
case 'S':
244244
it = skipNonDigits(it, end);
245245
second = parseNumberN(dtStr, it, end, 2);
246+
// Consume optional fractional seconds ('.NNN' or ',NNN') so that a
247+
// subsequent %z specifier can reach the timezone designator.
248+
// A decimal point/comma not followed by a digit is an error.
249+
if (it != end && (*it == '.' || *it == ','))
250+
{
251+
++it;
252+
if (it == end || !Ascii::isDigit(*it))
253+
{
254+
throw SyntaxException("Invalid DateTimeString: " + dtStr + ", missing fractional digits");
255+
}
256+
it = skipDigits(it, end);
257+
}
246258
break;
247259
case 's':
248260
it = skipNonDigits(it, end);

Foundation/src/Timezone_UNIX.cpp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <ctime>
1919
#include <cstdlib>
2020
#include <string>
21+
#include <sys/stat.h>
2122

2223

2324
namespace Poco {
@@ -66,13 +67,44 @@ class TZInfo
6667
{
6768
const char* tz = std::getenv("TZ");
6869
_cachedTZ = tz ? tz : "";
70+
// /etc/localtime is the system-wide zone source (updated by timedatectl etc.).
71+
// Stat'ing it lets us detect zone changes that don't touch the TZ env var; we
72+
// don't read its contents — tzset()/libc still resolves the actual zone data.
73+
cacheLocaltimeStat();
6974
}
7075

7176
bool tzChanged() const
7277
{
7378
const char* tz = std::getenv("TZ");
7479
std::string currentTZ = tz ? tz : "";
75-
return currentTZ != _cachedTZ;
80+
if (currentTZ != _cachedTZ) return true;
81+
return localtimeStatChanged();
82+
}
83+
84+
void cacheLocaltimeStat()
85+
{
86+
struct stat st{};
87+
if (::stat("/etc/localtime", &st) == 0)
88+
{
89+
_localtimeIno = st.st_ino;
90+
_localtimeMtime = st.st_mtime;
91+
}
92+
else
93+
{
94+
_localtimeIno = 0;
95+
_localtimeMtime = 0;
96+
}
97+
}
98+
99+
bool localtimeStatChanged() const
100+
{
101+
struct stat st{};
102+
if (::stat("/etc/localtime", &st) == 0)
103+
{
104+
return st.st_ino != _localtimeIno || st.st_mtime != _localtimeMtime;
105+
}
106+
// /etc/localtime not accessible: treat as changed only if we had it before
107+
return _localtimeIno != 0;
76108
}
77109

78110
static int computeTimeZone()
@@ -112,6 +144,8 @@ class TZInfo
112144
std::mutex _mutex;
113145
int _tzOffset;
114146
std::string _cachedTZ;
147+
ino_t _localtimeIno = 0;
148+
time_t _localtimeMtime = 0;
115149
};
116150

117151

Foundation/testsuite/src/DateTimeParserTest.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,37 @@ void DateTimeParserTest::testCustom()
638638
}
639639

640640

641+
void DateTimeParserTest::testISO8601FracSeconds()
642+
{
643+
// ISO8601_FORMAT uses %S which silently discards a well-formed fractional-second
644+
// suffix so that the trailing %z can still reach the timezone designator.
645+
int tzd;
646+
647+
// Dot-separated fractional seconds with negative timezone offset
648+
DateTime dt = DateTimeParser::parse(DateTimeFormat::ISO8601_FORMAT, "2013-10-07T08:23:19.120-04:00", tzd);
649+
assertTrue (dt.year() == 2013);
650+
assertTrue (dt.month() == 10);
651+
assertTrue (dt.day() == 7);
652+
assertTrue (dt.hour() == 8);
653+
assertTrue (dt.minute() == 23);
654+
assertTrue (dt.second() == 19);
655+
assertTrue (tzd == -4*3600);
656+
657+
// Comma-separated fractional seconds (ISO 8601 allows ',' as the decimal sign)
658+
dt = DateTimeParser::parse(DateTimeFormat::ISO8601_FORMAT, "2013-10-07T08:23:19,120-04:00", tzd);
659+
assertTrue (dt.year() == 2013);
660+
assertTrue (dt.month() == 10);
661+
assertTrue (dt.day() == 7);
662+
assertTrue (dt.hour() == 8);
663+
assertTrue (dt.minute() == 23);
664+
assertTrue (dt.second() == 19);
665+
assertTrue (tzd == -4*3600);
666+
667+
// A bare decimal point not followed by any digit must be rejected
668+
testBad(DateTimeFormat::ISO8601_FORMAT, "2013-10-07T08:23:19.-04:00", tzd);
669+
}
670+
671+
641672
void DateTimeParserTest::testGuess()
642673
{
643674
int tzd;
@@ -917,6 +948,7 @@ CppUnit::Test* DateTimeParserTest::suite()
917948
CppUnit_addTest(pSuite, DateTimeParserTest, testASCTIME);
918949
CppUnit_addTest(pSuite, DateTimeParserTest, testSORTABLE);
919950
CppUnit_addTest(pSuite, DateTimeParserTest, testCustom);
951+
CppUnit_addTest(pSuite, DateTimeParserTest, testISO8601FracSeconds);
920952
CppUnit_addTest(pSuite, DateTimeParserTest, testGuess);
921953
CppUnit_addTest(pSuite, DateTimeParserTest, testCleanup);
922954
CppUnit_addTest(pSuite, DateTimeParserTest, testParseMonth);

Foundation/testsuite/src/DateTimeParserTest.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class DateTimeParserTest: public CppUnit::TestCase
3434
void testASCTIME();
3535
void testSORTABLE();
3636
void testCustom();
37+
void testISO8601FracSeconds();
3738
void testGuess();
3839
void testCleanup();
3940
void testParseMonth();

0 commit comments

Comments
 (0)