From e2c03ab053f571e410b374e482ff4486d8882e6c Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Tue, 4 Aug 2026 14:12:37 +0200 Subject: [PATCH 1/5] datetime: treat military and unknown zone names as -0000 The parser matched zone names by their first one or two letters, so IST came out as military zone I (-0900) and CEST as CDT. RFC 5322 section 4.3 says both military zones and unrecognized names are to mean -0000. --- src/vmime/dateTime.cpp | 190 ++++++++-------------------------- tests/parser/datetimeTest.cpp | 23 ++++ 2 files changed, 65 insertions(+), 148 deletions(-) diff --git a/src/vmime/dateTime.cpp b/src/vmime/dateTime.cpp index 9dde881b..f166c092 100644 --- a/src/vmime/dateTime.cpp +++ b/src/vmime/dateTime.cpp @@ -75,6 +75,44 @@ static const char* monthNames[] = { "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + +static bool isNameEqualNoCase(const char* s, const size_t n, const char* name) { + + for (size_t i = 0 ; i < n ; ++i) { + if (name[i] == '\0' || + parserHelpers::toLower(s[i]) != parserHelpers::toLower(name[i])) { + return false; + } + } + + return name[n] == '\0'; +} + + +// RFC 5322 section 4.3: military zones were specified incorrectly in RFC 822, +// and other names are ambiguous; both are to be treated as "-0000". +static int zoneFromName(const char* s, const size_t n) { + + static const struct { + const char* name; + int zone; + } zoneNames[] = { + { "UT", datetime::UT }, { "GMT", datetime::GMT }, + { "EST", datetime::EST }, { "EDT", datetime::EDT }, + { "CST", datetime::CST }, { "CDT", datetime::CDT }, + { "MST", datetime::MST }, { "MDT", datetime::MDT }, + { "PST", datetime::PST }, { "PDT", datetime::PDT }, + }; + + for (const auto& z : zoneNames) { + if (isNameEqualNoCase(s, n, z.name)) { + return z.zone; + } + } + + return datetime::GMT; +} + void datetime::parseImpl( parsingContext& /* ctx */, const string& buffer, @@ -412,158 +450,14 @@ void datetime::parseImpl( m_zone = -(hourOff * 60 + minOff); } - } else if (p < pend && isalpha(*p)) { - - bool done = false; + } else if (p < pend && parserHelpers::isAlpha(*p)) { // Zone offset (Time zone name) - char_t zone[4] = { 0 }; - int zoneLength = 0; - - do { - zone[zoneLength++] = *p; - ++p; - } while (zoneLength < 3 && p < pend); - - switch (zone[0]) - { - case 'c': - case 'C': - { - if (zoneLength >= 2) - { - if (zone[1] == 's' || zone[1] == 'S') - m_zone = CST; - else - m_zone = CDT; - - done = true; - } - - break; - } - case 'e': - case 'E': - { - if (zoneLength >= 2) { - - if (zone[1] == 's' || zone[1] == 'S') { - m_zone = EST; - } else { - m_zone = EDT; - } - - done = true; - } - - break; - } - case 'm': - case 'M': { - - if (zoneLength >= 2) { + const char* zoneStart = p; - if (zone[1] == 's' || zone[1] == 'S') { - m_zone = MST; - } else { - m_zone = MDT; - } - - done = true; - } - - break; - } - case 'p': - case 'P': { - - if (zoneLength >= 2) { - - if (zone[1] == 's' || zone[1] == 'S') { - m_zone = PST; - } else { - m_zone = PDT; - } - - done = true; - } - - break; - } - case 'g': - case 'G': - case 'u': - case 'U': { - - if (zoneLength >= 2) { - - m_zone = GMT; // = UTC - done = true; - } - - break; - } - - } - - if (!done) { - - const char_t z = zone[0]; - - // Military time zone - if (z != 'j' && z != 'J') { - - typedef std::map Map; - static const Map::value_type offsetMapInit[] = { - - Map::value_type('a', -60), - Map::value_type('b', -120), - Map::value_type('c', -180), - Map::value_type('d', -240), - Map::value_type('e', -300), - Map::value_type('f', -360), - Map::value_type('g', -420), - Map::value_type('h', -480), - Map::value_type('i', -540), - Map::value_type('k', -600), - Map::value_type('l', -660), - Map::value_type('m', -720), - - Map::value_type('n', 60), - Map::value_type('o', 120), - Map::value_type('p', 180), - Map::value_type('q', 240), - Map::value_type('r', 300), - Map::value_type('s', 360), - Map::value_type('t', 420), - Map::value_type('u', 480), - Map::value_type('v', 540), - Map::value_type('w', 600), - Map::value_type('x', 660), - Map::value_type('y', 720), - - Map::value_type('z', 0), - }; - - static const Map offsetMap( - ::vmime::begin(offsetMapInit), - ::vmime::end(offsetMapInit) - ); - - Map::const_iterator pos = - offsetMap.find(parserHelpers::toLower(z)); - - if (pos != offsetMap.end()) { - m_zone = (*pos).second; - } else { - m_zone = GMT; - } - - } else { + while (p < pend && parserHelpers::isAlpha(*p)) ++p; - m_zone = GMT; - } - } + m_zone = zoneFromName(zoneStart, p - zoneStart); } else { diff --git a/tests/parser/datetimeTest.cpp b/tests/parser/datetimeTest.cpp index 61c85221..a1c64609 100644 --- a/tests/parser/datetimeTest.cpp +++ b/tests/parser/datetimeTest.cpp @@ -28,6 +28,7 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) VMIME_TEST_LIST_BEGIN VMIME_TEST(testParse) + VMIME_TEST(testParseZoneName) VMIME_TEST(testGenerate) VMIME_TEST(testCompare) VMIME_TEST_LIST_END @@ -77,6 +78,28 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) } } + void testParseZoneName() { + + vmime::datetime d; + + d.parse("3 Jul 2026 05:00:00 EST"); + VASSERT_EQ("1", vmime::datetime::EST, d.getZone()); + d.parse("3 Jul 2026 05:00:00 pdt"); + VASSERT_EQ("2", vmime::datetime::PDT, d.getZone()); + d.parse("3 Jul 2026 10:00:00 UT"); + VASSERT_EQ("3", vmime::datetime::UT, d.getZone()); + + // RFC 5322 section 4.3: to be treated as -0000 + d.parse("3 Jul 2026 10:00:00 A"); + VASSERT_EQ("4", 0, d.getZone()); + d.parse("3 Jul 2026 10:00:00 IST"); + VASSERT_EQ("5", 0, d.getZone()); + d.parse("3 Jul 2026 10:00:00 CEST"); + VASSERT_EQ("6", 0, d.getZone()); + d.parse("3 Jul 2026 10:00:00 E"); + VASSERT_EQ("7", 0, d.getZone()); + } + void testGenerate() { vmime::datetime d1(2005, 7, 8, 4, 5, 6, 1 * 60 + 23); From 7b78d74540e8fdb45220ea207748e3c8fd85a6cd Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Wed, 5 Aug 2026 10:47:09 +0200 Subject: [PATCH 2/5] datetime: parse RFC 5322 strictly and report the end in newPosition The parser always claimed to have consumed the whole input, and its recovery for ill-formed dates produces plausible but wrong values, so a caller could not tell a date from garbage. Try the RFC 5322 grammar first. Only if that fails, fall back to the old heuristics and leave newPosition at the start. --- src/vmime/dateTime.cpp | 289 +++++++++++++++++++++++++++++++++- src/vmime/dateTime.hpp | 4 + tests/parser/datetimeTest.cpp | 104 ++++++++++++ 3 files changed, 395 insertions(+), 2 deletions(-) diff --git a/src/vmime/dateTime.cpp b/src/vmime/dateTime.cpp index f166c092..9b1e1db5 100644 --- a/src/vmime/dateTime.cpp +++ b/src/vmime/dateTime.cpp @@ -113,6 +113,268 @@ static int zoneFromName(const char* s, const size_t n) { return datetime::GMT; } + +static constexpr const char* dayNamesLong[] = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" +}; + +static constexpr const char* monthNamesLong[] = { + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +}; + + +// Skip folding white space and (nested) comments +static const char* skipCFWS(const char* p, const char* const pend) { + + while (p < pend) { + + if (parserHelpers::isSpace(*p)) { + ++p; + continue; + } else if (*p != '(') { + break; + } + + int depth = 0; + const char* q = p; + + for ( ; q < pend ; ++q) { + + if (*q == '\\' && q + 1 < pend) { + ++q; + } else if (*q == '(') { + ++depth; + } else if (*q == ')' && --depth == 0) { + break; + } + } + + if (q == pend) { + break; + } + + p = q + 1; + } + + return p; +} + + +static bool parseNumber( + const char*& p, const char* const pend, + const size_t minDigits, const size_t maxDigits, int& value +) { + + const char* q = p; + int n = 0; + + while (q < pend && parserHelpers::isDigit(*q)) { + + if (static_cast (q - p) == maxDigits) { + return false; + } + + n = n * 10 + (*q - '0'); + ++q; + } + + if (static_cast (q - p) < minDigits) { + return false; + } + + p = q; + value = n; + + return true; +} + + +// Returns the index of the matching short or long name, or -1 +static int parseName( + const char*& p, const char* const pend, + const char*const * shortNames, const char*const * longNames, const int count +) { + + const char* q = p; + + while (q < pend && parserHelpers::isAlpha(*q)) ++q; + + for (int i = 0 ; i < count ; ++i) { + + if (isNameEqualNoCase(p, q - p, shortNames[i]) || + isNameEqualNoCase(p, q - p, longNames[i])) { + + p = q; + return i; + } + } + + return -1; +} + + +static bool parseTime( + const char*& p, const char* const pend, + int& hour, int& minute, int& second +) { + + if (!parseNumber(p, pend, 1, 2, hour) || hour > 23) { + return false; + } + + p = skipCFWS(p, pend); + + if (p == pend || *p != ':') { + return false; + } + + p = skipCFWS(p + 1, pend); + + if (!parseNumber(p, pend, 1, 2, minute) || minute > 59) { + return false; + } + + second = 0; + + const char* q = skipCFWS(p, pend); + + if (q < pend && *q == ':') { + + p = skipCFWS(q + 1, pend); + + // 60 is a leap second + if (!parseNumber(p, pend, 1, 2, second) || second > 60) { + return false; + } + } + + return true; +} + + +static bool parseZone(const char*& p, const char* const pend, int& zone) { + + if (p < pend && (*p == '+' || *p == '-')) { + + const char sign = *p; + const char* q = p + 1; + int offset = 0; + + if (!parseNumber(q, pend, 4, 4, offset) || offset % 100 > 59) { + return false; + } + + zone = (offset / 100) * 60 + offset % 100; + + if (sign == '-') { + zone = -zone; + } + + p = q; + return true; + } + + const char* q = p; + + while (q < pend && parserHelpers::isAlpha(*q)) ++q; + + if (q == p) { + return false; + } + + zone = zoneFromName(p, q - p); + p = q; + + return true; +} + + +struct datetimeFields { + int year, month, day, hour, minute, second, zone; +}; + + +// RFC 5322 section 3.3 date-time, including the obsolete syntax of 4.3. +// On success, *pnext is set past the date-time and any trailing CFWS. +static bool parseRFC5322( + const char* p, const char* const pend, + datetimeFields& f, const char** pnext +) { + + p = skipCFWS(p, pend); + + if (parseName(p, pend, dayNames, dayNamesLong, 7) >= 0) { + + p = skipCFWS(p, pend); + + if (p == pend || *p != ',') { + return false; + } + + p = skipCFWS(p + 1, pend); + } + + if (!parseNumber(p, pend, 1, 2, f.day) || f.day < 1) { + return false; + } + + p = skipCFWS(p, pend); + + if ((f.month = parseName(p, pend, monthNames, monthNamesLong, 12) + 1) == 0) { + return false; + } + + p = skipCFWS(p, pend); + + const char* yearStart = p; + + if (!parseNumber(p, pend, 2, 9, f.year)) { + return false; + } + + if (p - yearStart == 2) { + f.year += (f.year < 50) ? 2000 : 1900; + } else if (p - yearStart == 3) { + f.year += 1900; + } + + if (f.day > utility::datetimeUtils::getDaysInMonth(f.year, f.month)) { + return false; + } + + p = skipCFWS(p, pend); + + if (!parseTime(p, pend, f.hour, f.minute, f.second)) { + return false; + } + + p = skipCFWS(p, pend); + + const char* zoneStart = p; + + if (!parseZone(p, pend, f.zone)) { + return false; + } + + // Tolerate a redundant zone name, as in "+0200 CEST" + if (*zoneStart == '+' || *zoneStart == '-') { + + const char* q = skipCFWS(p, pend); + const char* r = q; + + while (r < pend && parserHelpers::isAlpha(*r)) ++r; + + if (r != q && skipCFWS(r, pend) == pend) { + p = r; + } + } + + *pnext = skipCFWS(p, pend); + + return true; +} + void datetime::parseImpl( parsingContext& /* ctx */, const string& buffer, @@ -124,7 +386,30 @@ void datetime::parseImpl( const char* const pend = buffer.data() + end; const char* p = buffer.data() + position; - // Parse the date and time value + datetimeFields f; + const char* pnext = NULL; + + if (parseRFC5322(p, pend, f, &pnext)) { + + m_year = f.year; + m_month = f.month; + m_day = f.day; + m_hour = f.hour; + m_minute = f.minute; + m_second = f.second; + m_zone = f.zone; + + setParsedBounds(position, pnext - buffer.data()); + + if (newPosition) { + *newPosition = pnext - buffer.data(); + } + + return; + } + + // Not well-formed. Make a best guess, but report that nothing + // was consumed. while (p < pend && parserHelpers::isSpace(*p)) ++p; if (p < pend) { @@ -480,7 +765,7 @@ void datetime::parseImpl( setParsedBounds(position, end); if (newPosition) { - *newPosition = end; + *newPosition = position; } } diff --git a/src/vmime/dateTime.hpp b/src/vmime/dateTime.hpp index 2a69cc2e..fc71ae2b 100644 --- a/src/vmime/dateTime.hpp +++ b/src/vmime/dateTime.hpp @@ -35,6 +35,10 @@ namespace vmime { /** Date and time (basic type). + * + * parse() sets newPosition past a well-formed RFC 5322 date-time. For + * anything else, it is left at the start position and the members hold + * a best guess. */ class VMIME_EXPORT datetime : public headerFieldValue { diff --git a/tests/parser/datetimeTest.cpp b/tests/parser/datetimeTest.cpp index a1c64609..3afd658b 100644 --- a/tests/parser/datetimeTest.cpp +++ b/tests/parser/datetimeTest.cpp @@ -29,6 +29,8 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) VMIME_TEST_LIST_BEGIN VMIME_TEST(testParse) VMIME_TEST(testParseZoneName) + VMIME_TEST(testParseWellFormed) + VMIME_TEST(testParseMalformed) VMIME_TEST(testGenerate) VMIME_TEST(testCompare) VMIME_TEST_LIST_END @@ -100,6 +102,108 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) VASSERT_EQ("7", 0, d.getZone()); } + static size_t parseEnd(vmime::datetime& d, const vmime::string& s) { + + vmime::parsingContext ctx; + size_t newPos = 0; + + d.parse(ctx, s, 0, s.length(), &newPos); + + return newPos; + } + + void testParseWellFormed() { + + vmime::datetime d; + vmime::string s; + + s = "Fri, 3 Jul 2026 10:00:00 +0000"; + VASSERT_EQ("1", s.length(), parseEnd(d, s)); + VASSERT_EQ("1v", vmime::datetime(2026, 7, 3, 10, 0, 0, 0), d); + + s = " (c) Fri (c) , 3 (c) jul 2026 10 : 00 (c) +0200 (a (nested) comment) "; + VASSERT_EQ("2", s.length(), parseEnd(d, s)); + VASSERT_EQ("2v", vmime::datetime(2026, 7, 3, 8, 0, 0, 0), d); + + s = "Friday, 3 July 2026 10:00 GMT"; + VASSERT_EQ("3", s.length(), parseEnd(d, s)); + VASSERT_EQ("3v", vmime::datetime(2026, 7, 3, 10, 0, 0, 0), d); + + // RFC 5322 section 4.3 year rules + s = "3 Jul 49 10:00:00 -0000"; + VASSERT_EQ("4", s.length(), parseEnd(d, s)); + VASSERT_EQ("4v", 2049, d.getYear()); + s = "3 Jul 50 10:00:00 -0000"; + VASSERT_EQ("5", s.length(), parseEnd(d, s)); + VASSERT_EQ("5v", 1950, d.getYear()); + s = "3 Jul 126 10:00:00 -0000"; + VASSERT_EQ("6", s.length(), parseEnd(d, s)); + VASSERT_EQ("6v", 2026, d.getYear()); + + s = "31 Dec 2016 23:59:60 +0000"; + VASSERT_EQ("7", s.length(), parseEnd(d, s)); + VASSERT_EQ("7v", 60, d.getSecond()); + + s = "29 Feb 2024 10:00:00 +0000"; + VASSERT_EQ("8", s.length(), parseEnd(d, s)); + + s = "3 Jul 2026 12:00:00 +0200 CEST"; + VASSERT_EQ("11", s.length(), parseEnd(d, s)); + VASSERT_EQ("11v", vmime::datetime(2026, 7, 3, 10, 0, 0, 0), d); + s = "3 Jul 2026 12:00:00 +0200 CEST (c)"; + VASSERT_EQ("12", s.length(), parseEnd(d, s)); + + // Trailing garbage is not consumed + s = "3 Jul 2026 10:00:00 +0000 ;xyz"; + VASSERT_EQ("9", s.length() - 4, parseEnd(d, s)); + VASSERT_EQ("9v", vmime::datetime(2026, 7, 3, 10, 0, 0, 0), d); + + s = "3 Jul 2026 10:00:00 GMT+0200"; + VASSERT_EQ("13", s.length() - 5, parseEnd(d, s)); + s = "3 Jul 2026 10:00:00 +0200 CEST x"; + VASSERT_EQ("14", s.length() - 6, parseEnd(d, s)); + + // Bounds are honored + s = "x 3 Jul 2026 10:00:00 +0000 x"; + size_t newPos = 0; + vmime::parsingContext ctx; + d.parse(ctx, s, 2, s.length() - 2, &newPos); + VASSERT_EQ("10", s.length() - 2, newPos); + } + + void testParseMalformed() { + + static const char* const inputs[] = { + "", + " ", + "not a date", + "by mx1.example.com id 12345", + "May 4 2026 10:00:00 +0000", + "Fri 3 Jul 2026 10:00:00 +0000", + "3 Jul 2026", + "3 Jul 2026 10:00:00", + "3 Jul 2026 10:00:00 +000", + "3 Jul 2026 10:00:00 +00000", + "3 Jul 2026 10:00:00 +0060", + "3 Jul 2026 24:00:00 +0000", + "3 Jul 2026 10:60:00 +0000", + "3 Jul 2026 10:00:61 +0000", + "0 Jul 2026 10:00:00 +0000", + "31 Jun 2026 10:00:00 +0000", + "29 Feb 2026 10:00:00 +0000", + "3 Jux 2026 10:00:00 +0000", + "3 Jul 2 10:00:00 +0000", + "Sat, 18, 2004 22:36:32 -0400", + }; + + for (unsigned int i = 0 ; i < sizeof(inputs) / sizeof(inputs[0]) ; ++i) { + + vmime::datetime d; + + VASSERT_EQ(inputs[i], 0, parseEnd(d, inputs[i])); + } + } + void testGenerate() { vmime::datetime d1(2005, 7, 8, 4, 5, 6, 1 * 60 + 23); From 733d42d8f86c62addbf1a17d111e3a5b9915af4d Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Wed, 5 Aug 2026 16:23:51 +0200 Subject: [PATCH 3/5] datetime: take the year from asctime-style dates "Thu Nov 18 12:11:16 2004" has the year after the time, which was ignored and replaced by the current year. Make it work. --- src/vmime/dateTime.cpp | 23 +++++++++++++++++++++-- tests/parser/datetimeTest.cpp | 9 ++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/vmime/dateTime.cpp b/src/vmime/dateTime.cpp index 9b1e1db5..5d8ff723 100644 --- a/src/vmime/dateTime.cpp +++ b/src/vmime/dateTime.cpp @@ -424,6 +424,7 @@ void datetime::parseImpl( } bool dayParsed = false; + bool yearAfterTime = false; if (parserHelpers::isAlpha(*p)) { @@ -598,9 +599,10 @@ void datetime::parseImpl( // Check for ill-formed date/time and try to recover if (p + 2 < pend && *(p + 2) == ':') { - // Skip year (default to current), and advance - // to time parsing + // Skip year (default to current, unless it follows + // the time as in asctime), and advance to time parsing m_year = now().getYear(); + yearAfterTime = true; } else { @@ -713,6 +715,23 @@ void datetime::parseImpl( while (p < pend && parserHelpers::isSpace(*p)) ++p; } + if (yearAfterTime && p + 3 < pend && parserHelpers::isDigit(*p)) { + + const char* q = p; + int year = 0; + + while (q < pend && parserHelpers::isDigit(*q)) { + year = year * 10 + (*q - '0'); + ++q; + } + + if (q - p == 4) { + m_year = year; + p = q; + while (p < pend && parserHelpers::isSpace(*p)) ++p; + } + } + if (p + 1 < pend && (*p == '+' || *p == '-') && parserHelpers::isDigit(*(p + 1))) { const char_t sign = *p; diff --git a/tests/parser/datetimeTest.cpp b/tests/parser/datetimeTest.cpp index 3afd658b..5f64b5b7 100644 --- a/tests/parser/datetimeTest.cpp +++ b/tests/parser/datetimeTest.cpp @@ -56,16 +56,19 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) vmime::datetime(2004, 11, 7, 0, 43, 22, vmime::datetime::GMT_5) }, { /* 3 */ "Thu Nov 18 12:11:16 2004", - vmime::datetime(vmime::datetime::now().getYear(), 11, 18, 12, 11, 16, vmime::datetime::GMT) }, + vmime::datetime(2004, 11, 18, 12, 11, 16, vmime::datetime::GMT) }, { /* 4 */ "Sat, 18, 2004 22:36:32 -0400", vmime::datetime(2004, 1, 18, 22, 36, 32, vmime::datetime::GMT_4) }, { /* 5 */ "Mon Dec 13 21:57:18 2004", - vmime::datetime(vmime::datetime::now().getYear(), 12, 13, 21, 57, 18, vmime::datetime::GMT) }, + vmime::datetime(2004, 12, 13, 21, 57, 18, vmime::datetime::GMT) }, { /* 6 */ "18 Nov 2004 21:44:54 +0300", - vmime::datetime(2004, 11, 18, 21, 44, 54, vmime::datetime::GMT3) } + vmime::datetime(2004, 11, 18, 21, 44, 54, vmime::datetime::GMT3) }, + + { /* 7 */ "Thu Nov 18 12:11:16 2004 +0100", + vmime::datetime(2004, 11, 18, 12, 11, 16, vmime::datetime::GMT1) } }; for (unsigned int i = 0 ; i < sizeof(pairs) / sizeof(pairs[0]) ; ++i) { From e23c3e5ba7705bf5524167ab3cc0fe6c21af1c19 Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Mon, 10 Aug 2026 11:05:44 +0200 Subject: [PATCH 4/5] datetime: honor the zone argument of datetime(time_t, zone) The broken-down UTC time was labeled with the given zone instead of being converted to it, yielding a different instant. --- src/vmime/dateTime.cpp | 14 ++++++++------ tests/parser/datetimeTest.cpp | 11 +++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vmime/dateTime.cpp b/src/vmime/dateTime.cpp index 5d8ff723..915b8135 100644 --- a/src/vmime/dateTime.cpp +++ b/src/vmime/dateTime.cpp @@ -869,25 +869,27 @@ datetime::datetime(const datetime& d) datetime::datetime(const time_t t, const int zone) { + const time_t tz = t + static_cast (zone) * 60; + #if VMIME_HAVE_LOCALTIME_S struct tm tms; - if (!gmtime_s(&tms, &t)) { - localtime_s(&tms, &t); + if (!gmtime_s(&tms, &tz)) { + localtime_s(&tms, &tz); } #elif VMIME_HAVE_LOCALTIME_R struct tm tms; - if (!gmtime_r(&t, &tms)) { - localtime_r(&t, &tms); + if (!gmtime_r(&tz, &tms)) { + localtime_r(&tz, &tms); } #else - struct tm* gtm = gmtime(&t); - struct tm* ltm = localtime(&t); + struct tm* gtm = gmtime(&tz); + struct tm* ltm = localtime(&tz); struct tm tms; diff --git a/tests/parser/datetimeTest.cpp b/tests/parser/datetimeTest.cpp index 5f64b5b7..3c27e379 100644 --- a/tests/parser/datetimeTest.cpp +++ b/tests/parser/datetimeTest.cpp @@ -32,6 +32,7 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) VMIME_TEST(testParseWellFormed) VMIME_TEST(testParseMalformed) VMIME_TEST(testGenerate) + VMIME_TEST(testFromTime) VMIME_TEST(testCompare) VMIME_TEST_LIST_END @@ -214,6 +215,16 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) VASSERT_EQ("1", "Fri, 8 Jul 2005 04:05:06 +0123", d1.generate()); } + void testFromTime() { + + VASSERT_EQ("1", "Fri, 3 Jul 2026 10:00:00 +0000", + vmime::datetime(1783072800).generate()); + VASSERT_EQ("2", "Fri, 3 Jul 2026 12:00:00 +0200", + vmime::datetime(1783072800, vmime::datetime::GMT2).generate()); + VASSERT_EQ("3", "Thu, 2 Jul 2026 23:30:00 -1030", + vmime::datetime(1783072800, -630).generate()); + } + void testCompare() { // Date1 = Date2 From d1cdb8c6d48dc1eac381a35ef039e7db7298e231 Mon Sep 17 00:00:00 2001 From: Mike Williams Date: Thu, 13 Aug 2026 15:52:03 +0200 Subject: [PATCH 5/5] datetime: avoid signed overflow on long digit runs The heuristic parser accumulated day, time, year and zone digits without bound. Limit them like the strict path does. --- src/vmime/dateTime.cpp | 16 ++++++++-------- tests/parser/datetimeTest.cpp | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vmime/dateTime.cpp b/src/vmime/dateTime.cpp index 915b8135..128f04cd 100644 --- a/src/vmime/dateTime.cpp +++ b/src/vmime/dateTime.cpp @@ -441,7 +441,7 @@ void datetime::parseImpl( int day = 0; do { - day = day * 10 + (*p - '0'); + if (day < 100000000) day = day * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -584,7 +584,7 @@ void datetime::parseImpl( int day = 0; do { - day = day * 10 + (*p - '0'); + if (day < 100000000) day = day * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -610,7 +610,7 @@ void datetime::parseImpl( int year = 0; do { - year = year * 10 + (*p - '0'); + if (year < 100000000) year = year * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -637,7 +637,7 @@ void datetime::parseImpl( int hour = 0; do { - hour = hour * 10 + (*p - '0'); + if (hour < 100000000) hour = hour * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -657,7 +657,7 @@ void datetime::parseImpl( int minute = 0; do { - minute = minute * 10 + (*p - '0'); + if (minute < 100000000) minute = minute * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -677,7 +677,7 @@ void datetime::parseImpl( int second = 0; do { - second = second * 10 + (*p - '0'); + if (second < 100000000) second = second * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); @@ -721,7 +721,7 @@ void datetime::parseImpl( int year = 0; while (q < pend && parserHelpers::isDigit(*q)) { - year = year * 10 + (*q - '0'); + if (year < 100000000) year = year * 10 + (*q - '0'); ++q; } @@ -741,7 +741,7 @@ void datetime::parseImpl( int offset = 0; do { - offset = offset * 10 + (*p - '0'); + if (offset < 100000000) offset = offset * 10 + (*p - '0'); ++p; } while (p < pend && parserHelpers::isDigit(*p)); diff --git a/tests/parser/datetimeTest.cpp b/tests/parser/datetimeTest.cpp index 3c27e379..6ed2d368 100644 --- a/tests/parser/datetimeTest.cpp +++ b/tests/parser/datetimeTest.cpp @@ -198,6 +198,8 @@ VMIME_TEST_SUITE_BEGIN(datetimeTest) "3 Jux 2026 10:00:00 +0000", "3 Jul 2 10:00:00 +0000", "Sat, 18, 2004 22:36:32 -0400", + "99999999999 Jul 99999999999 99999999999:99999999999:99999999999 +99999999999", + "Thu Nov 99999999999 12:11:16 99999999999", }; for (unsigned int i = 0 ; i < sizeof(inputs) / sizeof(inputs[0]) ; ++i) {